From fa3dc4425d64803b69a2e92f91e52f9ffedb8001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 12 May 2025 15:32:47 +0200 Subject: [PATCH 01/18] Initial support for Packet Filter --- Cargo.lock | 313 ++++--- Cargo.toml | 4 + src/enterprise/firewall/api.rs | 50 +- src/enterprise/firewall/dummy/mod.rs | 13 +- src/enterprise/firewall/iprange.rs | 132 +++ src/enterprise/firewall/mod.rs | 108 ++- .../firewall/{linux => nftables}/mod.rs | 27 +- .../firewall/{linux => nftables}/netfilter.rs | 116 ++- src/enterprise/firewall/packetfilter/calls.rs | 814 ++++++++++++++++++ src/enterprise/firewall/packetfilter/mod.rs | 218 +++++ src/enterprise/firewall/packetfilter/rule.rs | 201 +++++ .../firewall/packetfilter/ticket.rs | 48 ++ src/gateway.rs | 50 +- src/main.rs | 2 +- 14 files changed, 1765 insertions(+), 331 deletions(-) create mode 100644 src/enterprise/firewall/iprange.rs rename src/enterprise/firewall/{linux => nftables}/mod.rs (90%) rename src/enterprise/firewall/{linux => nftables}/netfilter.rs (91%) create mode 100644 src/enterprise/firewall/packetfilter/calls.rs create mode 100644 src/enterprise/firewall/packetfilter/mod.rs create mode 100644 src/enterprise/firewall/packetfilter/rule.rs create mode 100644 src/enterprise/firewall/packetfilter/ticket.rs diff --git a/Cargo.lock b/Cargo.lock index f5684811..03b20791 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "async-stream" @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core 0.5.2", "axum-macros", @@ -242,9 +242,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -281,9 +281,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.18" +version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" dependencies = [ "jobserver", "libc", @@ -304,9 +304,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.35" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -447,16 +447,18 @@ dependencies = [ name = "defguard-gateway" version = "1.3.0" dependencies = [ - "axum 0.8.3", + "axum 0.8.4", "base64", "clap", "defguard_wireguard_rs", "env_logger", "gethostname", "ipnetwork", + "libc", "log", "mnl", "nftnl", + "nix 0.30.1", "prost", "prost-build", "serde", @@ -485,7 +487,7 @@ dependencies = [ "netlink-packet-utils", "netlink-packet-wireguard", "netlink-sys", - "nix", + "nix 0.29.0", "serde", "thiserror 2.0.12", "x25519-dalek", @@ -671,9 +673,9 @@ dependencies = [ [[package]] name = "gethostname" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7131e57abbde63513e0e6636f76668a1ca9798dcae2df4e283cae9ee83859e" +checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" dependencies = [ "rustix", "windows-targets", @@ -681,9 +683,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -692,9 +694,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", @@ -710,9 +712,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git2" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ "bitflags", "libc", @@ -723,9 +725,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -748,9 +750,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "heck" @@ -871,21 +873,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -894,31 +897,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -926,67 +909,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1006,9 +976,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1031,7 +1001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1063,9 +1033,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.6" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" +checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" dependencies = [ "jiff-static", "log", @@ -1076,9 +1046,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.6" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" +checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" dependencies = [ "proc-macro2", "quote", @@ -1091,15 +1061,15 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libgit2-sys" @@ -1127,15 +1097,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "log" @@ -1178,9 +1148,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -1219,9 +1189,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "netlink-packet-core" @@ -1331,6 +1301,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1442,6 +1424,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1469,9 +1460,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1570,7 +1561,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -1610,7 +1601,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -1633,9 +1624,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", @@ -1646,9 +1637,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "log", "once_cell", @@ -1682,15 +1673,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -1861,9 +1855,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -1878,9 +1872,9 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1901,12 +1895,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1987,9 +1981,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -1997,9 +1991,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -2045,9 +2039,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -2058,9 +2052,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -2070,18 +2064,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap 2.9.0", "serde", @@ -2247,12 +2241,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2273,9 +2261,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.4" +version = "9.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d2f179f8075b805a43a2a21728a46f0cc2921b3c58695b28fa8817e103cd9a" +checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" dependencies = [ "anyhow", "derive_builder", @@ -2286,9 +2274,9 @@ dependencies = [ [[package]] name = "vergen-git2" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86bae87104cb2790cdee615c2bb54729804d307191732ab27b1c5357ea6ddc5" +checksum = "4f6ee511ec45098eabade8a0750e76eec671e7fb2d9360c563911336bea9cac1" dependencies = [ "anyhow", "derive_builder", @@ -2424,9 +2412,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -2440,17 +2428,11 @@ dependencies = [ "bitflags", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x25519-dalek" @@ -2466,9 +2448,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -2478,9 +2460,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -2490,18 +2472,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", @@ -2549,11 +2531,22 @@ dependencies = [ "syn", ] +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -2562,9 +2555,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 9a74686d..e94c4eab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", env_logger = "0.11" gethostname = "1.0" ipnetwork = "0.21" +libc = { version = "0.2", default-features = false } log = "0.4" prost = "0.13" serde = { version = "1.0", features = ["derive"] } @@ -25,6 +26,9 @@ toml = { version = "0.8", default-features = false, features = ["parse"] } nftnl = { git = "https://github.com/DefGuard/nftnl-rs.git", rev = "1a1147271f43b9d7182a114bb056a5224c35d38f" } mnl = "0.2" +[target.'cfg(any(target_os = "freebsd", target_os = "macos"))'.dependencies] +nix = { version = "0.30", default-features = false, features = ["ioctl"] } + [dev-dependencies] tokio = { version = "1", features = ["io-std", "io-util"] } x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"] } diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index 1950ea7e..1bc9c391 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -1,39 +1,65 @@ +#[cfg(any(target_os = "freebsd", target_os = "macos"))] +use std::fs::{File, OpenOptions}; + #[cfg(target_os = "linux")] use nftnl::Batch; use super::{FirewallError, FirewallRule, Policy}; +#[cfg(any(target_os = "freebsd", target_os = "macos"))] +const DEV_PF: &str = "/dev/pf"; + pub struct FirewallApi { - pub ifname: String, + pub(crate) ifname: String, + #[cfg(any(target_os = "freebsd", target_os = "macos"))] + file: File, #[cfg(target_os = "linux")] - #[allow(dead_code)] pub(crate) batch: Option, } impl FirewallApi { #[must_use] - pub fn new(ifname: &str) -> Self { - Self { + pub fn new>(ifname: S) -> Result { + Ok(Self { ifname: ifname.into(), + #[cfg(any(target_os = "freebsd", target_os = "macos"))] + file: OpenOptions::new() + .read(true) + .write(true) + .open(DEV_PF) + .map_err(|err| FirewallError::Io(err))?, #[cfg(target_os = "linux")] batch: None, - } + }) } } -pub trait FirewallManagementApi { - /// Sets up the firewall with the default policy and cleans up any existing rules - fn setup( - &mut self, - default_policy: Option, - priority: Option, - ) -> Result<(), FirewallError>; +pub(crate) trait FirewallManagementApi { + /// Set up the firewall with `default_policy`, `priority`, and cleans up any existing rules. + fn setup(&mut self, default_policy: Policy, priority: Option) + -> Result<(), FirewallError>; + + /// Clean up the firewall rules. fn cleanup(&mut self) -> Result<(), FirewallError>; + + /// Add firewall `rule`. fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError>; + + /// Add fireall `rules`. fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError>; + + /// Set default firewall policy. fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError>; + + /// Set masquerade status. fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError>; + + /// Begin rule transaction. fn begin(&mut self) -> Result<(), FirewallError>; + + /// Commit rule transaction. fn commit(&mut self) -> Result<(), FirewallError>; + + /// Rollback rule transaction. fn rollback(&mut self); } diff --git a/src/enterprise/firewall/dummy/mod.rs b/src/enterprise/firewall/dummy/mod.rs index e09b3919..e3a30a2a 100644 --- a/src/enterprise/firewall/dummy/mod.rs +++ b/src/enterprise/firewall/dummy/mod.rs @@ -1,13 +1,12 @@ use super::{ api::{FirewallApi, FirewallManagementApi}, - FirewallError, FirewallRule, Policy, Protocol, + FirewallError, FirewallRule, Policy, }; -use crate::proto; impl FirewallManagementApi for FirewallApi { fn setup( &mut self, - _default_policy: Option, + _default_policy: Policy, _priority: Option, ) -> Result<(), FirewallError> { Ok(()) @@ -43,11 +42,3 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } } - -impl Protocol { - pub const fn from_proto( - proto: proto::enterprise::firewall::Protocol, - ) -> Result { - Ok(Self(proto as u8)) - } -} diff --git a/src/enterprise/firewall/iprange.rs b/src/enterprise/firewall/iprange.rs new file mode 100644 index 00000000..722099b2 --- /dev/null +++ b/src/enterprise/firewall/iprange.rs @@ -0,0 +1,132 @@ +use std::{ + fmt, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ops::Range, +}; + +#[derive(Clone, Debug, PartialEq)] +pub enum IpAddrRange { + V4(Range), + V6(Range), +} + +#[derive(Debug, thiserror::Error)] +pub enum IpAddrRangeError { + MixedTypes, + WrongOrder, +} + +impl fmt::Display for IpAddrRangeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MixedTypes => write!(f, "mixed IPv4 and IPv6 addresses"), + Self::WrongOrder => write!(f, "wrong order: higher address preceeds lower"), + } + } +} + +impl IpAddrRange { + pub fn new(start: IpAddr, end: IpAddr) -> Result { + if start > end { + Err(IpAddrRangeError::WrongOrder) + } else { + match (start, end) { + (IpAddr::V4(start), IpAddr::V4(end)) => Ok(Self::V4(Range { start, end })), + (IpAddr::V6(start), IpAddr::V6(end)) => Ok(Self::V6(Range { start, end })), + _ => Err(IpAddrRangeError::MixedTypes), + } + } + } + + /// Returns `true` if `ipaddr` is contained in the range. + pub fn contains(&self, ipaddr: &IpAddr) -> bool { + match self { + Self::V4(range) => range.contains(ipaddr), + Self::V6(range) => range.contains(ipaddr), + } + } + + /// Returns `true` if the range contains no items. + pub fn is_empty(&self) -> bool { + match self { + Self::V4(range) => range.is_empty(), + Self::V6(range) => range.is_empty(), + } + } + + /// Returns `true` if range contains IPv4 address, and `false` otherwise. + pub fn is_ipv4(&self) -> bool { + match self { + Self::V4(_) => true, + Self::V6(_) => false, + } + } + + /// Returns `true` if range contains IPv6 address, and `false` otherwise. + pub fn is_ipv6(&self) -> bool { + match self { + Self::V4(_) => false, + Self::V6(_) => true, + } + } +} + +impl Iterator for IpAddrRange { + type Item = IpAddr; + + fn next(&mut self) -> Option { + match self { + Self::V4(range) => range.next().map(IpAddr::V4), + Self::V6(range) => range.next().map(IpAddr::V6), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_range() { + let start = IpAddr::V4(Ipv4Addr::LOCALHOST); + let end = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)); + let range = Range { start, end }; + + let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + assert!(range.contains(&a)); + + let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); + assert!(!range.contains(&a)); + + // As of Rust 1.86, `IpAddr` does not implement `Step`. + // assert_eq!(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), range.next()); + } + + #[test] + fn test_v4() { + let start = Ipv4Addr::LOCALHOST; + let end = Ipv4Addr::new(127, 0, 0, 3); + let mut range = Range { start, end }; + + assert_eq!(Some(Ipv4Addr::new(127, 0, 0, 1)), range.next()); + assert_eq!(Some(Ipv4Addr::new(127, 0, 0, 2)), range.next()); + assert_eq!(None, range.next()); + } + + #[test] + fn test_ipaddrrange() { + let start = IpAddr::V4(Ipv4Addr::LOCALHOST); + let end = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)); + let mut range = IpAddrRange::new(start, end).unwrap(); + + let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + assert!(range.contains(&a)); + + let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); + assert!(!range.contains(&a)); + + assert_eq!(Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), range.next()); + assert_eq!(Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2))), range.next()); + assert_eq!(None, range.next()); + } +} diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index f68c6b85..0d00586b 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -1,29 +1,31 @@ +pub mod api; +#[cfg(test)] +mod dummy; +mod iprange; +#[cfg(all(not(test), target_os = "linux"))] +mod nftables; +#[cfg(all(not(test), any(target_os = "freebsd", target_os = "macos")))] +mod packetfilter; + use std::{net::IpAddr, str::FromStr}; use ipnetwork::IpNetwork; +use iprange::{IpAddrRange, IpAddrRangeError}; use thiserror::Error; use crate::proto; -pub mod api; -#[cfg(all(not(test), target_os = "linux"))] -pub mod linux; - -#[cfg(any(test, not(target_os = "linux")))] -pub mod dummy; - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Address { - Ip(IpAddr), +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Address { Network(IpNetwork), - Range(IpAddr, IpAddr), + Range(IpAddrRange), } impl Address { pub fn from_proto(ip: &proto::enterprise::firewall::IpAddress) -> Result { match &ip.address { Some(proto::enterprise::firewall::ip_address::Address::Ip(ip)) => { - Ok(Self::Ip(IpAddr::from_str(ip).map_err(|err| { + Ok(Self::Network(IpNetwork::from_str(ip).map_err(|err| { FirewallError::TypeConversionError(format!("Invalid IP format: {err}")) })?)) } @@ -44,9 +46,9 @@ impl Address { "Invalid IP range: start IP ({start}) is greater than end IP ({end})", ))); } - Ok(Self::Range(start, end)) + Ok(Self::Range(IpAddrRange::new(start, end)?)) } - _ => Err(FirewallError::TypeConversionError(format!( + None => Err(FirewallError::TypeConversionError(format!( "Invalid IP address type. Must be one of Ip, IpSubnet, IpRange. Instead got {:?}", ip.address ))), @@ -54,8 +56,9 @@ impl Address { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Port { +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) enum Port { + Any, // currently it is handled with empty Vec Single(u16), Range(u16, u16), } @@ -99,26 +102,44 @@ impl Port { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Protocol(pub u8); - -// Protocols that have the concept of ports -pub const PORT_PROTOCOLS: [Protocol; 2] = [ - // TCP - Protocol(6), - // UDP - Protocol(17), -]; +/// As defined in `netinet/in.h`. +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u8)] +pub(crate) enum Protocol { + Any = libc::IPPROTO_IP as u8, + Icmp = libc::IPPROTO_ICMP as u8, + Tcp = libc::IPPROTO_TCP as u8, + Udp = libc::IPPROTO_UDP as u8, + IcmpV6 = libc::IPPROTO_ICMPV6 as u8, +} impl Protocol { #[must_use] - pub fn supports_ports(&self) -> bool { - PORT_PROTOCOLS.contains(self) + pub(crate) fn supports_ports(&self) -> bool { + match self { + Protocol::Tcp | Protocol::Udp => true, + _ => false, + } + } +} + +impl Protocol { + pub(crate) const fn from_proto( + proto: proto::enterprise::firewall::Protocol, + ) -> Result { + match proto { + proto::enterprise::firewall::Protocol::Tcp => Ok(Self::Tcp), + proto::enterprise::firewall::Protocol::Udp => Ok(Self::Udp), + proto::enterprise::firewall::Protocol::Icmp => Ok(Self::Icmp), + proto::enterprise::firewall::Protocol::Invalid => { + Err(FirewallError::UnsupportedProtocol(proto as u8)) + } + } } } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum Policy { +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub(crate) enum Policy { #[default] Allow, Deny, @@ -144,8 +165,8 @@ impl Policy { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FirewallRule { +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct FirewallRule { pub comment: Option, pub destination_addrs: Vec
, pub destination_ports: Vec, @@ -154,15 +175,15 @@ pub struct FirewallRule { pub protocols: Vec, pub source_addrs: Vec
, /// Whether a rule uses IPv4 (true) or IPv6 (false) - pub ipv4: bool, + pub ipv4: bool, // FIXME: is that really needed? } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FirewallConfig { +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct FirewallConfig { pub rules: Vec, pub default_policy: Policy, /// Whether the rules use IPv4 (true) or IPv6 (false) - pub ipv4: bool, + pub ipv4: bool, // TODO: remove } impl FirewallConfig { @@ -176,7 +197,10 @@ impl FirewallConfig { Policy::from_proto(config.default_policy.try_into().map_err(|err| { FirewallError::TypeConversionError(format!("Invalid default policy: {err:?}")) })?); - debug!("Using IPv4: {v4:?}, default firewall policy defined: {default_policy:?}. Proceeding to parsing rules..."); + debug!( + "Using IPv4: {v4:?}, default firewall policy defined: {default_policy:?}. \ + Proceeding to parsing rules..." + ); for rule in config.rules { debug!("Parsing the following received Defguard ACL proto rule: {rule:?}"); @@ -238,17 +262,25 @@ impl FirewallConfig { #[derive(Debug, Error)] pub enum FirewallError { + #[error("IP address range: {0}")] + IpAddrRange(#[from] IpAddrRangeError), + #[error("Io error: {0}")] + Io(#[from] std::io::Error), #[error("Type conversion error: {0}")] TypeConversionError(String), #[error("Out of memory: {0}")] OutOfMemory(String), #[error("Unsupported protocol: {0}")] UnsupportedProtocol(u8), + #[cfg(target_os = "linux")] #[error("Netlink error: {0}")] NetlinkError(String), #[error("Invalid configuration: {0}")] InvalidConfiguration(String), - #[error("Firewall transaction not started. Start the firewall transaction first in order to interact with the firewall API.")] + #[error( + "Firewall transaction not started. Start the firewall transaction first in order to \ + interact with the firewall API." + )] TransactionNotStarted, #[error("Firewall transaction failed: {0}")] TransactionFailed(String), diff --git a/src/enterprise/firewall/linux/mod.rs b/src/enterprise/firewall/nftables/mod.rs similarity index 90% rename from src/enterprise/firewall/linux/mod.rs rename to src/enterprise/firewall/nftables/mod.rs index 12f7e142..d627a5f1 100644 --- a/src/enterprise/firewall/linux/mod.rs +++ b/src/enterprise/firewall/nftables/mod.rs @@ -2,7 +2,6 @@ pub mod netfilter; use std::sync::atomic::{AtomicU32, Ordering}; -use mnl::mnl_sys::libc; use netfilter::{ allow_established_traffic, apply_filter_rules, drop_table, ignore_unrelated_traffic, init_firewall, send_batch, set_default_policy, set_masq, @@ -11,9 +10,8 @@ use nftnl::Batch; use super::{ api::{FirewallApi, FirewallManagementApi}, - Address, FirewallError, FirewallRule, Policy, Port, Protocol, PORT_PROTOCOLS, + Address, FirewallError, FirewallRule, Policy, Port, Protocol, }; -use crate::proto; static SET_ID_COUNTER: AtomicU32 = AtomicU32::new(0); @@ -21,21 +19,6 @@ pub fn get_set_id() -> u32 { SET_ID_COUNTER.fetch_add(1, Ordering::Relaxed) } -impl Protocol { - pub const fn from_proto( - proto: proto::enterprise::firewall::Protocol, - ) -> Result { - match proto { - proto::enterprise::firewall::Protocol::Tcp => Ok(Self(libc::IPPROTO_TCP as u8)), - proto::enterprise::firewall::Protocol::Udp => Ok(Self(libc::IPPROTO_UDP as u8)), - proto::enterprise::firewall::Protocol::Icmp => Ok(Self(libc::IPPROTO_ICMP as u8)), - proto::enterprise::firewall::Protocol::Invalid => { - Err(FirewallError::UnsupportedProtocol(proto as u8)) - } - } - } -} - #[derive(Debug, Default)] pub enum State { #[default] @@ -74,10 +57,10 @@ impl FirewallManagementApi for FirewallApi { /// This allows for making atomic changes to the firewall rules. fn setup( &mut self, - default_policy: Option, + default_policy: Policy, priority: Option, ) -> Result<(), FirewallError> { - debug!("Initializing firewall, VPN interface: {}", self.ifname); + debug!("Initializing firewall, VPN interface: {}", &self.ifname); if let Some(batch) = &mut self.batch { drop_table(batch)?; init_firewall(default_policy, priority, batch).expect("Failed to setup chains"); @@ -200,8 +183,8 @@ impl FirewallManagementApi for FirewallApi { debug!( "Destination ports specified, but no protocols specified, applying nftables rules for each protocol that support ports." ); - for protocol in PORT_PROTOCOLS { - debug!("Applying nftables rule for protocol: {:?}", protocol); + for protocol in [Protocol::Tcp, Protocol::Udp] { + debug!("Applying nftables rule for protocol: {protocol:?}"); let rule = FilterRule { src_ips: &rule.source_addrs, dest_ips: &rule.destination_addrs, diff --git a/src/enterprise/firewall/linux/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs similarity index 91% rename from src/enterprise/firewall/linux/netfilter.rs rename to src/enterprise/firewall/nftables/netfilter.rs index 840c0879..19986818 100644 --- a/src/enterprise/firewall/linux/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -1,14 +1,14 @@ #[cfg(test)] use std::str::FromStr; use std::{ - ffi::CString, + ffi::{CStr, CString}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ops::Range, }; use ipnetwork::IpNetwork; #[cfg(test)] use ipnetwork::{Ipv4Network, Ipv6Network}; -use mnl::mnl_sys::libc::{self}; use nftnl::{ expr::{Expression, InterfaceName}, nft_expr, nftnl_sys, @@ -17,14 +17,14 @@ use nftnl::{ }; use super::{get_set_id, Address, FilterRule, Policy, Port, Protocol, State}; -use crate::enterprise::firewall::FirewallError; +use crate::enterprise::firewall::{iprange::IpAddrRange, FirewallError}; const FILTER_TABLE: &str = "filter"; const NAT_TABLE: &str = "nat"; const DEFGUARD_TABLE: &str = "DEFGUARD"; const POSTROUTING_CHAIN: &str = "POSTROUTING"; const FORWARD_CHAIN: &str = "FORWARD"; -const ANON_SET_NAME: &str = "__set%d"; +const ANON_SET_NAME: &CStr = c"__set%d"; const LOOPBACK_IFACE: &str = "lo"; const POSTROUTING_PRIORITY: i32 = 100; @@ -54,10 +54,10 @@ impl State { impl Protocol { pub(crate) fn as_port_payload_expr(&self) -> Result<&impl Expression, FirewallError> { - match self.0.into() { - libc::IPPROTO_TCP => Ok(&nft_expr!(payload tcp dport)), - libc::IPPROTO_UDP => Ok(&nft_expr!(payload udp dport)), - _ => Err(FirewallError::UnsupportedProtocol(self.0)), + match self { + Self::Tcp => Ok(&nft_expr!(payload tcp dport)), + Self::Udp => Ok(&nft_expr!(payload udp dport)), + _ => Err(FirewallError::UnsupportedProtocol(*self as u8)), } } } @@ -76,7 +76,7 @@ impl SetKey for Protocol { const TYPE: u32 = 12; fn data(&self) -> Box<[u8]> { - Box::new([self.0]) + Box::new([*self as u8]) } } @@ -90,28 +90,6 @@ pub trait FirewallRule { fn add_address_to_set(set: *mut nftnl_sys::nftnl_set, ip: &Address) -> Result<(), FirewallError> { match ip { - Address::Ip(ip) => match ip { - IpAddr::V4(ip) => { - add_to_set(set, ip, Some(ip))?; - } - IpAddr::V6(ip) => { - add_to_set(set, ip, Some(ip))?; - } - }, - Address::Range(start, end) => match (start, end) { - (IpAddr::V4(start), IpAddr::V4(end)) => { - add_to_set(set, start, Some(end))?; - } - (IpAddr::V6(start), IpAddr::V6(end)) => { - add_to_set(set, start, Some(end))?; - } - _ => { - return Err(FirewallError::InvalidConfiguration(format!( - "Expected both addresses to be of the same type, got {:?} and {:?}", - start, end - ))) - } - }, Address::Network(network) => { let upper_bound = max_address(network); let net = network.network(); @@ -124,12 +102,20 @@ fn add_address_to_set(set: *mut nftnl_sys::nftnl_set, ip: &Address) -> Result<() } _ => { return Err(FirewallError::InvalidConfiguration(format!( - "Expected both addresses to be of the same type, got {:?} and {:?}", - net, upper_bound + "Expected both addresses to be of the same type, got {net:?} and \ + {upper_bound:?}", ))) } } } + Address::Range(addr_range) => match addr_range { + IpAddrRange::V4(Range { start, end }) => { + add_to_set(set, start, Some(end))?; + } + IpAddrRange::V6(Range { start, end }) => { + add_to_set(set, start, Some(end))?; + } + }, } Ok(()) @@ -137,6 +123,9 @@ fn add_address_to_set(set: *mut nftnl_sys::nftnl_set, ip: &Address) -> Result<() fn add_port_to_set(set: *mut nftnl_sys::nftnl_set, port: &Port) -> Result<(), FirewallError> { match port { + Port::Any => { + // nothing to do + } Port::Single(port) => { let inet_service = InetService(*port); add_to_set(set, &inet_service, Some(&inet_service))?; @@ -167,15 +156,16 @@ impl FirewallRule for FilterRule<'_> { batch: &mut Batch, ) -> Result, FirewallError> { let mut rule = Rule::new(chain); - debug!("Converting {:?} to nftables expression", self); + debug!("Converting {self:?} to nftables expression"); // Debug purposes only let mut matches = Vec::new(); if !self.dest_ports.is_empty() && self.protocols.len() > 1 { - return Err(FirewallError::InvalidConfiguration( - format!("Cannot specify multiple protocols with destination ports, specified protocols: {:?}, destination ports: {:?}, Defguard Rule ID: {}", - self.protocols, self.dest_ports, self.defguard_rule_id) - )); + return Err(FirewallError::InvalidConfiguration(format!( + "Cannot specify multiple protocols with destination ports, specified \ + protocols: {:?}, destination ports: {:?}, Defguard Rule ID: {}", + self.protocols, self.dest_ports, self.defguard_rule_id + ))); } // TODO: Reduce code duplication here @@ -323,14 +313,15 @@ impl FirewallRule for FilterRule<'_> { }); rule.add_expr(&nft_expr!(meta l4proto)); - rule.add_expr(&nft_expr!(cmp == protocol.0)); + rule.add_expr(&nft_expr!(cmp == *protocol as u8)); rule.add_expr(protocol.as_port_payload_expr()?); rule.add_expr(&nft_expr!(lookup & set)); } } debug!( - "Added single protocol ({:?}) match and destination ports match to nftables expression: {:?}", + "Added single protocol ({:?}) match and destination ports match to nftables \ + expression: {:?}", self.protocols, self.dest_ports ); matches.push(format!( @@ -352,9 +343,9 @@ impl FirewallRule for FilterRule<'_> { rule.add_expr(&nft_expr!(payload ipv6 nextheader)); } - rule.add_expr(&nft_expr!(cmp == protocol.0)); - debug!("Added protocol match to rule: {:?}", protocol); - matches.push(format!("SINGLE PROTOCOL: {:?}", protocol)); + rule.add_expr(&nft_expr!(cmp == *protocol as u8)); + debug!("Added protocol match to rule: {protocol:?}"); + matches.push(format!("SINGLE PROTOCOL: {protocol:?}")); } } @@ -367,8 +358,8 @@ impl FirewallRule for FilterRule<'_> { } else { rule.add_expr(&nft_expr!(cmp == exact)); } - debug!("Added input interface match to rule: {:?}", iifname); - matches.push(format!("INPUT INTERFACE: {:?}", iifname)); + debug!("Added input interface match to rule: {iifname:?}"); + matches.push(format!("INPUT INTERFACE: {iifname:?}")); } if let Some(oifname) = &self.oifname { @@ -380,8 +371,8 @@ impl FirewallRule for FilterRule<'_> { } else { rule.add_expr(&nft_expr!(cmp == exact)); } - debug!("Added output interface match to rule: {:?}", oifname); - matches.push(format!("OUTPUT INTERFACE: {:?}", oifname)); + debug!("Added output interface match to rule: {oifname:?}"); + matches.push(format!("OUTPUT INTERFACE: {oifname:?}")); } if !self.states.is_empty() { @@ -418,10 +409,7 @@ impl FirewallRule for FilterRule<'_> { // comment if let Some(comment_string) = &self.comment { - debug!( - "Adding comment to nftables expression: {:?}", - comment_string - ); + debug!("Adding comment to nftables expression: {comment_string:?}"); // Since we are interoping with C, truncate the string to 255 *bytes* (not UTF-8 characters) // 256 is the maximum length of a comment string in nftables, leave 1 byte for the null terminator let maybe_truncated_str = if comment_string.len() > 255 { @@ -442,7 +430,7 @@ impl FirewallRule for FilterRule<'_> { } let matches = matches.join(" AND "); - debug!("Created nftables rule with matches: {:?}", matches); + debug!("Created nftables rule with matches: {matches:?}"); Ok(rule) } @@ -560,7 +548,7 @@ impl FirewallRule for NatRule { /// Sets up the default chains for the firewall pub(crate) fn init_firewall( - initial_policy: Option, + initial_policy: Policy, defguard_fwd_chain_priority: Option, batch: &mut Batch, ) -> Result<(), FirewallError> { @@ -575,7 +563,7 @@ pub(crate) fn init_firewall( nftnl::Hook::Forward, defguard_fwd_chain_priority.unwrap_or(FORWARD_PRIORITY), ); - fw_chain.set_policy(initial_policy.unwrap_or(Policy::Allow).into()); + fw_chain.set_policy(initial_policy.into()); fw_chain.set_type(nftnl::ChainType::Filter); batch.add(&fw_chain, nftnl::MsgType::Add); @@ -815,16 +803,16 @@ fn socket_recv<'a>( fn max_address(network: &IpNetwork) -> IpAddr { match network { IpNetwork::V4(network) => { - let ip_u32 = u32::from(network.ip()); - let mask_u32 = u32::from(network.mask()); + let addr = network.ip().to_bits(); + let mask = network.mask().to_bits(); - IpAddr::V4(Ipv4Addr::from(ip_u32 | !mask_u32)) + IpAddr::V4(Ipv4Addr::from(addr | !mask)) } IpNetwork::V6(network) => { - let ip_u128 = u128::from(network.ip()); - let mask_u128 = u128::from(network.mask()); + let addr = network.ip().to_bits(); + let mask = network.mask().to_bits(); - IpAddr::V6(Ipv6Addr::from(ip_u128 | !mask_u128)) + IpAddr::V6(Ipv6Addr::from(addr | !mask)) } } } @@ -837,13 +825,7 @@ fn new_anon_set( where T: SetKey, { - let set = Set::::new( - &CString::new(ANON_SET_NAME) - .expect("Failed to create CString from ANON_SET_NAME constant."), - get_set_id(), - table, - family, - ); + let set = Set::::new(ANON_SET_NAME, get_set_id(), table, family); if interval_set { unsafe { diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs new file mode 100644 index 00000000..44792d42 --- /dev/null +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -0,0 +1,814 @@ +use std::{ + ffi::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong, c_ushort, c_void}, + mem::{size_of, MaybeUninit}, +}; + +use ipnetwork::IpNetwork; +use libc::{pid_t, sa_family_t, uid_t, IFNAMSIZ}; +use nix::{ioctl_none, ioctl_readwrite}; + +use super::rule::{Action, AddressFamily, Direction, PacketFilterRule, RuleSet, State}; +use crate::enterprise::firewall::Port; + +/// Equivalent to `struct pf_addr`: fits 128-bit address, either IPv4 or IPv6. +type Addr = [u8; 16]; // Do not use u128 for the sake of alignment. +/// Equivalent to `pf_poolhashkey`: 128-bit hash key. +type PoolHashKey = [u8; 16]; + +const PF_TABLE_NAME_SIZE: usize = 32; +#[cfg(target_os = "macos")] +const RTLABEL_LEN: usize = 32; + +/// Equivalent to `struct pf_addr_wrap_addr_mask`. +#[derive(Debug)] +#[repr(C)] +struct AddrMask { + addr: Addr, + mask: Addr, +} + +impl From for AddrMask { + fn from(ip_network: IpNetwork) -> Self { + match ip_network { + IpNetwork::V4(ipnet4) => { + let mut addr_mask = Self { + addr: [0; 16], + mask: [0; 16], + }; + // Fill the first 4 bytes of `addr` and `mask`. + addr_mask.addr[..4].copy_from_slice(&ipnet4.ip().octets()); + addr_mask.mask[..4].copy_from_slice(&ipnet4.mask().octets()); + + addr_mask + } + + IpNetwork::V6(ipnet6) => Self { + addr: ipnet6.ip().octets(), + mask: ipnet6.mask().octets(), + }, + } + } +} + +/// Equivalent to `struct pf_addr_wrap`. +/// Only the `v` part of the union, as `p` is not used in this crate. +#[derive(Debug)] +#[repr(C)] +pub struct AddrWrap { + v: AddrMask, + // pub v: pf_addr_wrap_v, + // unused in this crate + // pub p: pf_addr_wrap_p, + p: u64, + r#type: AddrType, + iflags: c_uchar, +} + +#[derive(Debug)] +#[repr(u8)] +pub enum AddrType { + // PF_ADDR_ADDRMASK = 0, + AddrMask, + // PF_ADDR_NOROUTE = 1, + NoRoute, + // PF_ADDR_DYNIFTL = 2, + DynIftl, + // PF_ADDR_TABLE = 3, + Table, + // Below differs on macOS and FreeBSD. + // PF_ADDR_RTLABEL = 4, + // RtLabel, + // // PF_ADDR_URPFFAILED = 5, + // UrpfFailed, + // // PF_ADDR_RANGE = 6, + // Range, +} + +impl AddrWrap { + #[must_use] + pub fn new(ip_network: IpNetwork) -> Self { + Self { + v: ip_network.into(), + p: 0, + r#type: AddrType::AddrMask, + iflags: 0, + } + } +} + +/// Equivalent to `struct pf_rule_addr`. +#[derive(Debug)] +#[repr(C)] +pub struct RuleAddr { + pub addr: AddrWrap, + // macOS: here `union pf_rule_xport` is flattened to its first variant: `struct pf_port_range`. + pub port: [c_ushort; 2], + pub op: PortOp, + #[cfg(target_os = "macos")] + _padding: [c_uchar; 3], + #[cfg(target_os = "macos")] + pub neg: c_uchar, +} + +impl RuleAddr { + #[must_use] + pub fn new(ip_network: IpNetwork, port: Port) -> Self { + let addr = AddrWrap::new(ip_network); + let from_port; + let to_port; + let op; + match port { + Port::Any => { + from_port = 0; + to_port = 0; + op = PortOp::None; + } + Port::Single(port) => { + from_port = port; + to_port = 0; + op = PortOp::None; + } + Port::Range(from, to) => { + from_port = from; + to_port = to; + op = PortOp::Equal; + } + } + Self { + addr, + port: [from_port, to_port], + op, + #[cfg(target_os = "macos")] + _padding: [0; 3], + #[cfg(target_os = "macos")] + neg: 0, + } + } +} + +/// TAILQ_ENTRY +#[derive(Debug)] +#[repr(C)] +pub struct pf_rule_list { + pub tqe_next: *mut Rule, + pub tqe_prev: *mut *mut Rule, +} + +#[repr(C)] +pub struct pf_pooladdr_list { + pub tqe_next: *mut PoolAddr, + pub tqe_prev: *mut *mut PoolAddr, +} + +// Equivalent to `struct pf_pooladdr`. +#[repr(C)] +pub struct PoolAddr { + addr: AddrWrap, + entries: pf_pooladdr_list, + ifname: [u8; IFNAMSIZ], + kif: usize, // *mut c_void, +} + +impl PoolAddr { + #[must_use] + pub fn new(ip_network: IpNetwork, if_name: &str) -> Self { + let mut ifname = [0; IFNAMSIZ]; + let len = if_name.len().min(IFNAMSIZ - 1); + ifname[..len].copy_from_slice(&if_name.as_bytes()[..len]); + Self { + addr: AddrWrap::new(ip_network), + entries: unsafe { std::mem::zeroed::() }, + ifname, + kif: 0, + } + } +} + +#[derive(Debug)] +#[repr(C)] +pub struct pf_palist { + pub tqh_first: *mut PoolAddr, + pub tqh_last: *mut *mut PoolAddr, +} + +/// Equivalent to `struct pf_pool`. +#[derive(Debug)] +#[repr(C)] +pub struct Pool { + pub list: pf_palist, + pub cur: *mut c_void, + pub key: PoolHashKey, + pub counter: Addr, + pub tblidx: c_int, + pub proxy_port: [c_ushort; 2], + #[cfg(target_os = "macos")] + pub port_op: PortOp, + pub opts: c_uchar, + #[cfg(target_os = "macos")] + pub af: sa_family_t, +} + +#[derive(Debug)] +#[repr(u8)] +pub enum PortOp { + /// PF_OP_NONE = 0 + None, + /// PF_OP_IRG = 1 + InclRange, // ((p > a1) && (p < a2)) + /// PF_OP_EQ = 2 + Equal, + /// PF_OP_NE = 3, + NotEqual, + /// PF_OP_LT = 4 + Less, + /// PF_OP_LE = 5 + LessOrEqual, + /// PF_OP_GT = 6 + Greater, + /// PF_OP_GE = 7 + GreaterOrEqual = 7, + /// PF_OP_XRG = 8 + ExclRange, // ((p < a1) || (p > a2)) + /// PF_OP_RRG = 9 + Range = 9, // ((p >= a1) && (p <= a2)) +} + +impl Pool { + #[must_use] + pub fn new(port: u16) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + unsafe { + (*self_ptr).proxy_port[0] = port; + } + + unsafe { uninit.assume_init() } + } +} + +#[repr(C)] +pub struct pf_anchor_global { + pub rbe_left: *mut pf_anchor, + pub rbe_right: *mut pf_anchor, + pub rbe_parent: *mut pf_anchor, +} + +#[repr(C)] +pub struct pf_anchor_node { + pub rbe_left: *mut pf_anchor, + pub rbe_right: *mut pf_anchor, + pub rbe_parent: *mut pf_anchor, +} + +#[repr(C)] +pub struct pf_rulequeue { + pub tqh_first: *mut Rule, + pub tqh_last: *mut *mut Rule, +} + +#[repr(C)] +pub struct pf_ruleset_rule { + pub ptr: *mut pf_rulequeue, + pub ptr_array: *mut *mut Rule, + pub rcount: c_uint, + pub rsize: c_uint, + pub ticket: c_uint, + pub open: c_int, +} + +#[repr(C)] +pub struct pf_ruleset_rules { + pub queues: [pf_rulequeue; 2], + pub active: pf_ruleset_rule, + pub inactive: pf_ruleset_rule, +} + +#[repr(C)] +pub struct pf_ruleset { + pub rules: [pf_ruleset_rules; 6], + pub anchor: *mut pf_anchor, + pub tticket: c_uint, + pub tables: c_int, + pub topen: c_int, +} + +#[repr(C)] +pub struct pf_anchor { + pub entry_global: pf_anchor_global, + pub entry_node: pf_anchor_node, + pub parent: *mut pf_anchor, + pub children: pf_anchor_node, + pub name: [c_char; 64], + pub path: [c_char; 1024], + pub ruleset: pf_ruleset, + pub refcnt: c_int, + pub match_: c_int, + pub owner: [c_char; 64], +} + +/// A packed Operating System description for fingerprinting. +type pf_osfp_t = c_uint; +// #define PF_OSFP_ANY ((pf_osfp_t)0) +// #define PF_OSFP_UNKNOWN ((pf_osfp_t)-1) +// #define PF_OSFP_NOMATCH ((pf_osfp_t)-2) + +#[derive(Debug)] +#[repr(C)] +pub struct pf_rule_conn_rate { + pub limit: c_uint, + pub seconds: c_uint, +} + +#[derive(Debug)] +#[repr(C)] +pub struct pf_rule_id { + pub uid: [uid_t; 2], + pub op: c_uchar, + // pub _pad: [u_int8_t; 3], +} + +/// As defined in `net/pfvar.h`. +const PF_RULE_LABEL_SIZE: usize = 64; + +/// Equivalent to 'struct pf_rule'. +#[derive(Debug)] +#[repr(C)] +pub struct Rule { + src: RuleAddr, + pub dst: RuleAddr, + + pub skip: [usize; 8], + pub label: [c_uchar; PF_RULE_LABEL_SIZE], + pub ifname: [c_uchar; IFNAMSIZ], + pub qname: [c_uchar; 64], + pub pqname: [c_uchar; 64], + pub tagname: [c_uchar; 64], + pub match_tagname: [c_uchar; 64], + pub overload_tblname: [c_uchar; 32], + + pub entries: pf_rule_list, + pub rpool: Pool, + + pub evaluations: c_long, + pub packets: [c_ulong; 2], + pub bytes: [c_ulong; 2], + + #[cfg(target_os = "macos")] + pub ticket: c_ulong, + #[cfg(target_os = "macos")] + pub owner: [c_char; 64], + #[cfg(target_os = "macos")] + pub priority: c_int, + + pub kif: *mut c_void, // struct pfi_kif, kernel only + pub anchor: *mut pf_anchor, + pub overload_tbl: *mut c_void, // struct pfr_ktable, kernel only + + pub os_fingerprint: pf_osfp_t, + + pub rtableid: c_uint, + #[cfg(target_os = "freebsd")] + pub timeout: [c_uint; 20], + #[cfg(target_os = "macos")] + pub timeout: [c_uint; 26], + #[cfg(target_os = "macos")] + pub states: c_uint, + pub max_states: c_uint, + #[cfg(target_os = "macos")] + pub src_nodes: c_uint, + pub max_src_nodes: c_uint, + pub max_src_states: c_uint, + pub max_src_conn: c_uint, + pub max_src_conn_rate: pf_rule_conn_rate, + pub qid: c_uint, + pub pqid: c_uint, + pub rt_listid: c_uint, + pub nr: c_uint, + pub prob: c_uint, + pub cuid: uid_t, + pub cpid: pid_t, + + #[cfg(target_os = "freebsd")] + pub states_cur: u64, + #[cfg(target_os = "freebsd")] + pub states_tot: u64, + #[cfg(target_os = "freebsd")] + pub src_nodes: u64, + + pub return_icmp: c_ushort, + pub return_icmp6: c_ushort, + pub max_mss: c_ushort, + pub tag: c_ushort, + pub match_tag: c_ushort, + #[cfg(target_os = "freebsd")] + pub scrub_flags: c_ushort, + + pub uid: pf_rule_id, + pub gid: pf_rule_id, + + pub rule_flag: c_uint, // RuleFlag + pub action: Action, + pub direction: Direction, + pub log: c_uchar, // LogFlags + pub logif: c_uchar, + pub quick: bool, + pub ifnot: c_uchar, + pub match_tag_not: c_uchar, + pub natpass: c_uchar, + + pub keep_state: State, + pub af: AddressFamily, // sa_family_t + pub proto: c_uchar, + pub(crate) r#type: c_uchar, + pub code: c_uchar, + pub flags: c_uchar, + pub flagset: c_uchar, + pub min_ttl: c_uchar, + pub allow_opts: c_uchar, + pub rt: c_uchar, + pub return_ttl: c_uchar, + + pub tos: c_uchar, + #[cfg(target_os = "freebsd")] + pub set_tos: c_uchar, + pub anchor_relative: c_uchar, + pub anchor_wildcard: c_uchar, + pub flush: c_uchar, + #[cfg(target_os = "freebsd")] + pub prio: c_uchar, + #[cfg(target_os = "freebsd")] + pub set_prio: [c_uchar; 2], + + #[cfg(target_os = "freebsd")] + pub divert: (pf_addr, u16), + + #[cfg(target_os = "freebsd")] + pub u_states_cur: u64, + #[cfg(target_os = "freebsd")] + pub u_states_tot: u64, + #[cfg(target_os = "freebsd")] + pub u_src_nodes: u64, + + #[cfg(target_os = "macos")] + pub proto_variant: c_uchar, + #[cfg(target_os = "macos")] + pub extfilter: c_uchar, + #[cfg(target_os = "macos")] + pub extmap: c_uchar, + #[cfg(target_os = "macos")] + pub dnpipe: c_uint, + #[cfg(target_os = "macos")] + pub dntype: c_uint, +} + +impl Rule { + // TODO: expand + #[must_use] + pub fn new(src: IpNetwork, src_port: Port) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + + unsafe { + (*self_ptr).dst = RuleAddr::new(src, src_port); + // Set address family. + // TODO: match empty network, then set AF_UNSPEC. + // (*self_ptr).af = match src { + // IpNetwork::V4(_) => AF_INET as u8, + // IpNetwork::V6(_) => AF_INET6 as u8, + // }; + (*self_ptr).keep_state = State::Normal; + + uninit.assume_init() + } + } + + pub fn from_rule(pf_rule: &PacketFilterRule) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + + unsafe { + if let Some(from) = pf_rule.from { + (*self_ptr).src = RuleAddr::new(from, pf_rule.from_port); + } + if let Some(to) = pf_rule.to { + (*self_ptr).dst = RuleAddr::new(to, pf_rule.to_port); + } + if let Some(label) = &pf_rule.label { + let len = label.len().min(PF_RULE_LABEL_SIZE - 1); + (*self_ptr).label[..len].copy_from_slice(&label.as_bytes()[..len]); + } + (*self_ptr).action = pf_rule.action; + (*self_ptr).direction = pf_rule.direction; + (*self_ptr).quick = pf_rule.quick; + (*self_ptr).af = pf_rule.address_family(); + + uninit.assume_init() + } + } +} + +/// Equivalent to PF_CHANGE_... enum. +#[repr(u32)] +pub(crate) enum Change { + // PF_CHANGE_NONE = 0 + None, + // PF_CHANGE_ADD_HEAD = 1 + AddHead, + // PF_CHANGE_ADD_TAIL = 2 + AddTail, + // PF_CHANGE_ADD_BEFORE = 3 + AddBefore, + // PF_CHANGE_ADD_AFTER = 4 + AddAfter, + // PF_CHANGE_REMOVE = 5 + Remove, + // PF_CHANGE_GET_TICKET = 6 + GetTicket, +} + +/// Rule flags, equivalent to PFRULE_... +#[repr(u32)] +pub(crate) enum RuleFlag { + Drop = 0, + ReturnRST = 1, + Fragment = 2, + ReturnICMP = 4, + Return = 8, + NoSync = 16, + SrcTrack = 32, + RuleSrcTrack = 64, + // ... +} + +// 1024 bytes +pub(crate) const MAXPATHLEN: usize = libc::PATH_MAX as usize; + +/// Equivalent to `struct pfioc_rule`. +#[repr(C)] +pub struct IocRule { + pub action: Change, + pub ticket: c_uint, + pub pool_ticket: c_uint, + pub nr: c_uint, + pub anchor: [c_uchar; MAXPATHLEN], + pub anchor_call: [c_uchar; MAXPATHLEN], + pub rule: Rule, +} + +impl IocRule { + #[must_use] + pub fn new(anchor: &str) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + + // Copy anchor name. + let len = anchor.len().min(MAXPATHLEN - 1); + unsafe { + (*self_ptr).anchor[..len].copy_from_slice(&anchor.as_bytes()[..len]); + } + + unsafe { uninit.assume_init() } + } + + #[must_use] + pub fn with_rule(anchor: &str, rule: Rule) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + + // Copy anchor name. + let len = anchor.len().min(MAXPATHLEN - 1); + unsafe { + (*self_ptr).anchor[..len].copy_from_slice(&anchor.as_bytes()[..len]); + (*self_ptr).rule = rule; + } + + unsafe { uninit.assume_init() } + } +} + +/// Equivalent to `struct pfioc_pooladdr`. +#[repr(C)] +pub struct IocPoolAddr { + action: Change, + pub ticket: c_uint, + nr: c_uint, + r_num: c_uint, + r_action: c_uchar, + r_last: c_uchar, + af: c_uchar, + anchor: [c_uchar; MAXPATHLEN], + pub addr: PoolAddr, +} + +impl IocPoolAddr { + #[must_use] + pub fn new(anchor: &str) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + + // Copy anchor name. + let len = anchor.len().min(MAXPATHLEN - 1); + unsafe { + (*self_ptr).anchor[..len].copy_from_slice(&anchor.as_bytes()[..len]); + } + + unsafe { uninit.assume_init() } + } + + // pub fn set_addr(&mut self, addr:) { + + // } +} + +/// Equivalent to `struct pfioc_trans_pfioc_trans_e`. +#[repr(C)] +pub struct IocTransElement { + rs_num: RuleSet, + anchor: [c_uchar; MAXPATHLEN], + pub ticket: c_uint, +} + +impl IocTransElement { + #[must_use] + pub fn new(ruleset: RuleSet, anchor: &str) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + + // Set `RuleSet` and copy anchor name. + let len = anchor.len().min(MAXPATHLEN - 1); + unsafe { + (*self_ptr).rs_num = ruleset; + (*self_ptr).anchor[..len].copy_from_slice(&anchor.as_bytes()[..len]); + } + + unsafe { uninit.assume_init() } + } +} + +/// Equivalent to `struct pfioc_trans`. +#[repr(C)] +pub struct IocTrans { + /// number of elements + size: c_int, + /// size of each element in bytes + esize: c_int, + array: *mut IocTransElement, +} + +impl IocTrans { + #[must_use] + pub fn new(elements: &mut [IocTransElement]) -> Self { + Self { + size: elements.len() as i32, + esize: size_of::() as i32, + array: elements.as_mut_ptr(), + } + } +} + +// fn setup_trans( +// pfioc_trans: &mut pfioc_trans, +// pfioc_trans_elements: &mut [ffi::pfvar::pfioc_trans_pfioc_trans_e], +// ) { +// pfioc_trans.size = pfioc_trans_elements.len() as i32; +// pfioc_trans.esize = size_of::() as i32; +// pfioc_trans.array = pfioc_trans_elements.as_mut_ptr(); +// } + +// DIOCSTART +ioctl_none!(pf_start, b'D', 1); + +// DIOCSTOP +ioctl_none!(pf_stop, b'D', 2); + +// DIOCADDRULE +ioctl_readwrite!(pf_add_rule, b'D', 4, IocRule); + +// DIOCGETRULES +ioctl_readwrite!(pf_get_rules, b'D', 6, IocRule); + +// DIOCGETRULE +ioctl_readwrite!(pf_get_rule, b'D', 7, IocRule); + +// DIOCCLRSTATES +// ioctl_readwrite!(pf_clear_states, b'D', 18, pfioc_state_kill); + +// DIOCGETSTATUS +// ioctl_readwrite!(pf_get_status, b'D', 21, pf_status); + +// DIOCGETSTATES (COMPAT_FREEBSD14) +// ioctl_readwrite!(pf_get_states, b'D', 25, pfioc_states); + +// DIOCCHANGERULE +ioctl_readwrite!(pf_change_rule, b'D', 26, IocRule); + +// DIOCINSERTRULE +// Substituted on FreeBSD, NetBSD, and OpenBSD by DIOCCHANGERULE with rule.action = PF_CHANGE_REMOVE +#[cfg(target_os = "macos")] +ioctl_readwrite!(pf_insert_rule, b'D', 27, IocRule); + +// DIOCDELETERULE +// Substituted on FreeBSD, NetBSD, and OpenBSD by DIOCCHANGERULE with rule.action = PF_CHANGE_REMOVE +#[cfg(target_os = "macos")] +ioctl_readwrite!(pf_delete_rule, b'D', 28, IocRule); + +// DIOCKILLSTATES +// ioctl_readwrite!(pf_kill_states, b'D', 41, pfioc_state_kill); + +// DIOCBEGINADDRS +// #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))] +ioctl_readwrite!(pf_begin_addrs, b'D', 51, IocPoolAddr); + +// DIOCADDADDR +// #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))] +ioctl_readwrite!(pf_add_addr, b'D', 52, IocPoolAddr); + +// DIOCGETRULESETS +// #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +// ioctl_readwrite!(pf_get_rulesets, b'D', 58, PFRuleset); + +// DIOCGETRULESET +// #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] +// ioctl_readwrite!(pf_get_ruleset, b'D', 59, PFRuleset); + +// DIOCXBEGIN +ioctl_readwrite!(pf_begin, b'D', 81, IocTrans); + +// DIOCXCOMMIT +ioctl_readwrite!(pf_commit, b'D', 82, IocTrans); + +// DIOCXEND +// Required by OpenBSD to release the ticket obtained by the DIOCGETRULES command. +// #[cfg(target_os = "openbsd")] +// ioctl_readwrite!(pf_end_trans, b'D', 100, c_int); + +#[cfg(test)] +mod tests { + use ipnetwork::{Ipv4Network, Ipv6Network}; + + use std::{ + mem::align_of, + net::{Ipv4Addr, Ipv6Addr}, + }; + + use super::*; + + #[test] + fn check_align_and_size() { + assert_eq!(align_of::(), 8); + assert_eq!(size_of::(), 48); + + assert_eq!(align_of::(), 8); + assert_eq!(size_of::(), 72); + + assert_eq!(align_of::(), 8); + assert_eq!(size_of::(), 16); + + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), 1032); + + assert_eq!(align_of::(), 8); + #[cfg(target_os = "freebsd")] + assert_eq!(size_of::(), 976); + #[cfg(target_os = "macos")] + assert_eq!(size_of::(), 1040); + + assert_eq!(align_of::(), 8); + #[cfg(target_os = "freebsd")] + assert_eq!(size_of::(), 56); + #[cfg(target_os = "macos")] + assert_eq!(size_of::(), 64); + + assert_eq!(align_of::(), 8); + #[cfg(target_os = "freebsd")] + assert_eq!(size_of::(), 3104); + #[cfg(target_os = "macos")] + assert_eq!(size_of::(), 3104); + } + + #[test] + fn check_pf_addr_wrap() { + let ipnetv4 = IpNetwork::V4(Ipv4Network::new(Ipv4Addr::LOCALHOST, 8).unwrap()); + + let addr_wrap = AddrWrap::new(ipnetv4); + assert_eq!( + addr_wrap.v.addr, + [127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + assert_eq!( + addr_wrap.v.mask, + [255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + + let ipv6 = IpNetwork::V6(Ipv6Network::new(Ipv6Addr::LOCALHOST, 32).unwrap()); + let addr_wrap = AddrWrap::new(ipv6); + assert_eq!( + addr_wrap.v.addr, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + ); + assert_eq!( + addr_wrap.v.mask, + [255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + } +} diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs new file mode 100644 index 00000000..470c88be --- /dev/null +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -0,0 +1,218 @@ +//! Interface to Packet Filter. +//! +//! Source code: +//! +//! Darwin: +//! - https://github.com/apple-oss-distributions/xnu/blob/main/bsd/net/pfvar.h +//! +//! FreeBSD: +//! - https://github.com/freebsd/freebsd-src/blob/main/sys/net/pfvar.h +//! - https://github.com/freebsd/freebsd-src/blob/main/sys/netpfil/pf/pf.h +//! +//! https://man.netbsd.org/pf.4 +//! https://man.freebsd.org/cgi/man.cgi?pf +//! https://man.openbsd.org/pf.4 + +mod calls; +mod rule; +mod ticket; + +use std::os::fd::AsRawFd; + +use ipnetwork::IpNetwork; + +use self::{ + calls::{ + pf_add_rule, pf_begin, pf_change_rule, pf_commit, Change, IocRule, IocTrans, + IocTransElement, Rule, + }, + rule::{Action, Direction, RuleSet}, + ticket::{get_pool_ticket, get_ticket}, +}; +use crate::enterprise::firewall::Port; + +use super::{ + api::{FirewallApi, FirewallManagementApi}, + FirewallError, FirewallRule, Policy, +}; + +/* +impl PacketFilter { + pub fn new() -> std::io::Result { + let file = OpenOptions::new().read(true).write(true).open(DEV_PF)?; + Ok(Self { file }) + } + + pub fn enable(&self) { + unsafe { + calls::pf_start(self.file.as_raw_fd()).unwrap(); + } + } + + /// Return ticket for filter rules. + pub fn begin(&self, anchor: &str) -> u32 { + let element = IocTransElement::new(RuleSet::Filter, anchor); + // let mut elements = vec![element]; + let mut elements = [element]; + let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); + + // This will create an anchor. + unsafe { + pf_begin(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); + } + + elements[0].ticket + } + + // TODO: expand + pub fn add_rule(&self, src: IpNetwork, src_port: Port, anchor: &str) { + // let ticket = self.begin(anchor); + + let element = IocTransElement::new(RuleSet::Filter, anchor); + // let mut elements = vec![element]; + let mut elements = [element]; + let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); + + // This will create an anchor. + unsafe { + pf_begin(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); + } + + let ticket = elements[0].ticket; + + // --- + let pool_ticket = get_pool_ticket(self.file.as_raw_fd(), anchor); + + let mut rule = Rule::new(src, src_port); + // rule.action = Change::AddTail; FreeBSD/OpenBSD only? + rule.direction = Direction::In; + + // eprintln!("Src {:?}", rule.src); + // eprintln!("Dst {:?}", rule.dst); + eprintln!("{:?}", rule); + + let mut ioc = IocRule::with_rule(anchor, rule); + ioc.action = Change::None; + ioc.ticket = ticket; + ioc.pool_ticket = pool_ticket; + + // pf_add_rule returns EBUSY on macOS. + unsafe { + pf_add_rule(self.file.as_raw_fd(), &mut ioc).unwrap(); + pf_commit(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); + } + } + + // TODO: expand + pub fn append_rule(&self, src: IpNetwork, src_port: Port, anchor: &str) { + // OpenBSD has no pool tickets + // #[cfg(any(target_os = "macos", target_os = "freebsd"))] + let pool_ticket = get_pool_ticket(self.file.as_raw_fd(), anchor); + let ticket = get_ticket(self.file.as_raw_fd(), anchor, Action::Pass); + + eprintln!("Ticket {ticket}, pool ticket {pool_ticket}"); + + let mut rule = Rule::new(src, src_port); + // rule.action = Change::AddTail; FreeBSD/OpenBSD only? + rule.direction = Direction::In; + + let mut ioc = IocRule::with_rule(anchor, rule); + ioc.action = Change::AddHead; + ioc.ticket = ticket; + ioc.pool_ticket = pool_ticket; + + // pf_add_rule returns EBUSY on macOS. + unsafe { + // pf_add_rule(self.file.as_raw_fd(), &mut ioc).unwrap(); + pf_change_rule(self.file.as_raw_fd(), &mut ioc).unwrap(); + } + } + + // Add anchor with a given `name`. + // FIXME: This method only works on macOS. + // pub fn add_anchor(&self, name: &str) { + // // #[cfg(target_os = "macos")] + // // { + // // pfioc_rule.rule.action = kind.into(); + // // } + // // #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] + // // { + // // pfioc_rule.rule.action = PF_CHANGE_REMOVE as u8; + // // } + + // // FIXME: empty + // let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); + // let mut rule = Rule::new(src); + + // #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] + // { + // rule.action = Action::NoNat; // = Change::Remove + // } + + // let mut ioc = IocRule { + // action: Change::None, + // ticket: 0, + // pool_ticket: 0, + // nr: 0, + // anchor: [0; 1024], + // anchor_call: [0; 1024], + // rule, + // }; + // name.bytes() + // .take(1023) + // .enumerate() + // .for_each(|(i, b)| ioc.anchor_call[i] = b); + + // // unsafe { pf_insert_rule(self.file.as_raw_fd(), &mut ioc).unwrap() }; + // unsafe { pf_change_rule(self.file.as_raw_fd(), &mut ioc).unwrap() }; + // } +} +*/ + +impl FirewallManagementApi for FirewallApi { + fn setup( + &mut self, + _default_policy: Policy, + _priority: Option, + ) -> Result<(), FirewallError> { + Ok(()) + } + + /// Clean up the firewall rules. + fn cleanup(&mut self) -> Result<(), FirewallError> { + Ok(()) + } + + /// Add firewall `rule`. + fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError> { + Ok(()) + } + + /// Add fireall `rules`. + fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { + Ok(()) + } + + /// Set default firewall policy. + fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { + Ok(()) + } + + /// Set masquerade status. + fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { + Ok(()) + } + + /// Begin rule transaction. + fn begin(&mut self) -> Result<(), FirewallError> { + Ok(()) + } + + /// Commit rule transaction. + fn commit(&mut self) -> Result<(), FirewallError> { + Ok(()) + } + + /// Rollback rule transaction. + fn rollback(&mut self) {} +} diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs new file mode 100644 index 00000000..41b961af --- /dev/null +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -0,0 +1,201 @@ +use ipnetwork::IpNetwork; +use libc::{AF_INET, AF_INET6, AF_UNSPEC}; + +use super::{FirewallRule, Port}; +use crate::enterprise::firewall::{Policy, Protocol}; + +/// Packet filter rule action. +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum Action { + /// PF_PASS = 0, + Pass, + // PF_DROP = 1, + Drop, + // PF_SCRUB = 2, + Scrub, + // PF_NOSCRUB = 3, + NoScrub, + // PF_NAT = 4, + Nat, + // PF_NONAT = 5, + NoNat, + // PF_BINAT = 6, + BiNat, + // PF_NOBINAT = 7, + NoBiNat, + // PF_RDR = 8, + Redirect, + // PF_NORDR = 9, + NoRedirect, + // PF_SYNPROXY_DROP = 10, + // PF_DUMMYNET = 11, + // PF_NODUMMYNET = 12, + // PF_NAT64 = 13, + // PF_NONAT64 = 14, +} + +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub(crate) enum AddressFamily { + Unspec = AF_UNSPEC as u8, + Inet = AF_INET as u8, + Inet6 = AF_INET6 as u8, +} + +/// Packet filter rule direction. +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum Direction { + /// PF_INOUT = 0 + InOut, + /// PF_IN = 1 + In, + /// PF_OUT = 2 + Out, +} + +/// TODO: convert to `const` +#[repr(u8)] +pub enum LogFlags { + /// PF_LOG = 0x01 + Log = 1, + /// PF_LOG_ALL = 0x02 + All = 2, + /// PF_LOG_SOCKET_LOOKUP = 0x04, + SocketLookup = 4, + /// PF_LOG_FORCE = 0x08 + #[cfg(target_os = "freebsd")] + Force = 8, + /// PF_LOG_MATCHES = 0x10 + #[cfg(target_os = "freebsd")] + Matches = 16, +} + +pub struct PacketFilterRule { + /// Source address; `Option::None` means "any". + pub(crate) from: Option, + /// Source port; 0 means "any". + pub(crate) from_port: Port, + /// Destination address; `Option::None` means "any". + pub(crate) to: Option, + /// Destination port; 0 means "any". + pub(crate) to_port: Port, + pub(crate) action: Action, + pub(crate) direction: Direction, + pub(crate) quick: bool, + /// See `LogFlags`. + pub(crate) log: u8, + pub(crate) keep_state: State, + pub(crate) interface: Option, + pub(crate) proto: Protocol, + /// See `TcpFlags`. + pub(crate) tcp_flags: u8, + pub(crate) label: Option, +} + +impl PacketFilterRule { + /// Determine address family based on `from` field. + pub(crate) fn address_family(&self) -> AddressFamily { + match self.from { + None => AddressFamily::Unspec, + Some(IpNetwork::V4(_)) => AddressFamily::Inet, + Some(IpNetwork::V6(_)) => AddressFamily::Inet6, + } + } + + pub(crate) fn from_firewall_rule(fr: FirewallRule) -> Vec { + let mut rules = Vec::new(); + let action = match fr.verdict { + Policy::Allow => Action::Pass, + Policy::Deny => Action::Drop, + }; + + // TODO: use Any source address + for dest in &fr.destination_addrs { + if fr.destination_ports.is_empty() { + // use Port::Any + let rule = Self { + from: None, + from_port: Port::Any, + to: None, + to_port: Port::Any, + action, + direction: Direction::InOut, + quick: false, + log: 0, + keep_state: State::None, + interface: None, + proto: Protocol::Any, // TODO: iterate + tcp_flags: 0, + label: fr.comment.clone(), + }; + rules.push(rule); + } else { + for port in &fr.destination_ports { + // + } + } + } + + rules + } +} + +/// Equivalent to `PF_RULESET_...`. +#[derive(Clone, Copy, Debug)] +#[repr(i32)] +pub enum RuleSet { + /// PF_RULESET_SCRUB = 0 + Scrub, + /// PF_RULESET_FILTER = 1 + Filter, + /// PF_RULESET_NAT = 2 + Nat, + /// PF_RULESET_BINAT = 3 + BiNat, + /// PF_RULESET_RDR = 4 + Redirect, + /// PF_RULESET_ALTQ = 5 + Altq, + /// PF_RULESET_TABLE = 6 + Table, + /// PF_RULESET_ETH = 7 + Eth, +} + +// Equivalent to `PF_STATE_...`. +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum State { + // Don't keep state. + None = 0, + // PF_STATE_NORMAL = 1 + Normal = 1, + // PF_STATE_MODULATE = 2 + Modulate = 2, + // PF_STATE_SYNPROXY = 3 + SynProxy = 3, +} + +/// As defined in `netinet/tcp.h`. +#[repr(u8)] +pub enum TcpFlags { + Any, + // TH_FIN = 0x01; Final: Set on the last segment. + Fin = 1, + // TH_SYN = 0x02; Synchronization: New conn with dst port. + Syn = 2, + // TH_RST = 0x04; Reset: Announce to peer conn terminated. + Rst = 4, + // TH_PUSH = 0x08; Push: Immediately send, don't buffer seg. + Push = 8, + // TH_ACK = 0x10; Acknowledge: Part of connection establish. + Ack = 16, + // TH_URG = 0x20; Urgent: send special marked segment now. + Urg = 32, + // TH_ECE = 0x40; ECN Echo. + Ece = 64, + // TH_CWR = 0x80; Congestion Window Reduced. + Cwr = 128, +} diff --git a/src/enterprise/firewall/packetfilter/ticket.rs b/src/enterprise/firewall/packetfilter/ticket.rs new file mode 100644 index 00000000..70111cec --- /dev/null +++ b/src/enterprise/firewall/packetfilter/ticket.rs @@ -0,0 +1,48 @@ +use std::os::fd::RawFd; + +use ipnetwork::IpNetwork; + +use super::{ + calls::{pf_add_addr, pf_begin_addrs, pf_change_rule, Change, IocPoolAddr, IocRule, PoolAddr}, + rule::Action, +}; + +pub fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> u32 { + let mut pfioc_rule = IocRule::new(anchor); + + pfioc_rule.action = Change::GetTicket; + pfioc_rule.rule.action = kind; + + // pfioc_rule.action is ignored on FreeBSD, NetBSD, and OpenBSD. --- REALLY? [adam] + // #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] + // { + // pfioc_rule.action = Change::GetTicket; + // pfioc_rule.rule.action = Change::GetTicket as u8; + // } + + unsafe { + pf_change_rule(fd, &mut pfioc_rule).unwrap(); + } + + pfioc_rule.ticket +} + +pub fn get_pool_ticket(fd: RawFd, anchor: &str) -> u32 { + let mut ioc = IocPoolAddr::new(anchor); + + unsafe { + pf_begin_addrs(fd, &mut ioc).unwrap(); + } + + ioc.ticket +} + +// Add pool address using the pool ticket previously obtained via `get_pool_ticket()` +pub fn add_pool_address(fd: RawFd, pool_addr: IpNetwork, pool_ticket: u32) { + let mut pfioc_pooladdr = unsafe { std::mem::zeroed::() }; + pfioc_pooladdr.ticket = pool_ticket; + pfioc_pooladdr.addr = PoolAddr::new(pool_addr, ""); // XXX: ifname + unsafe { + pf_add_addr(fd, &mut pfioc_pooladdr).unwrap(); + } +} diff --git a/src/gateway.rs b/src/gateway.rs index e4fc7fda..cad52db1 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -321,7 +321,7 @@ impl Gateway { debug!("Received firewall configuration is different than current one. Reconfiguring firewall..."); self.firewall_api.begin()?; self.firewall_api - .setup(Some(fw_config.default_policy), self.config.fw_priority)?; + .setup(fw_config.default_policy, self.config.fw_priority)?; if self.config.masquerade { self.firewall_api.set_masquerade_status(true)?; } @@ -612,6 +612,8 @@ impl Gateway { #[cfg(test)] mod tests { + use std::net::Ipv4Addr; + #[cfg(not(target_os = "macos"))] use defguard_wireguard_rs::Kernel; #[cfg(target_os = "macos")] @@ -657,7 +659,7 @@ mod tests { let wgapi = WGApi::::new("wg0".into()).unwrap(); let config = Config::default(); let client = Gateway::setup_client(&config).unwrap(); - let firewall_api = FirewallApi::new("wg0"); + let firewall_api = FirewallApi::new("wg0").unwrap(); let gateway = Gateway { config, interface_configuration: Some(old_config.clone()), @@ -787,23 +789,31 @@ mod tests { let rule1 = FirewallRule { comment: Some("Rule 1".to_string()), - destination_addrs: vec![Address::Ip(IpAddr::from_str("10.0.0.1").unwrap())], + destination_addrs: vec![Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 32).unwrap(), + )], destination_ports: vec![Port::Single(80)], id: 1, verdict: Policy::Allow, - protocols: vec![Protocol(6)], // TCP - source_addrs: vec![Address::Ip(IpAddr::from_str("192.168.1.1").unwrap())], + protocols: vec![Protocol::Tcp], + source_addrs: vec![Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 32).unwrap(), + )], ipv4: true, }; let rule2 = FirewallRule { comment: Some("Rule 2".to_string()), - destination_addrs: vec![Address::Ip(IpAddr::from_str("10.0.0.2").unwrap())], + destination_addrs: vec![Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2)), 32).unwrap(), + )], destination_ports: vec![Port::Single(443)], id: 2, verdict: Policy::Allow, - protocols: vec![Protocol(6)], // TCP - source_addrs: vec![Address::Ip(IpAddr::from_str("192.168.1.2").unwrap())], + protocols: vec![Protocol::Tcp], + source_addrs: vec![Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 32).unwrap(), + )], ipv4: true, }; @@ -815,7 +825,7 @@ mod tests { destination_ports: vec![Port::Range(1000, 2000)], id: 3, verdict: Policy::Deny, - protocols: vec![Protocol(17)], // UDP + protocols: vec![Protocol::Udp], source_addrs: vec![Address::Network( IpNetwork::from_str("192.168.0.0/16").unwrap(), )], @@ -829,7 +839,7 @@ mod tests { }; let config_empty = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, ipv4: true, }; @@ -849,7 +859,7 @@ mod tests { connected: Arc::new(AtomicBool::new(false)), client, stats_thread: None, - firewall_api: FirewallApi::new("test_interface"), + firewall_api: FirewallApi::new("test_interface").unwrap(), firewall_config: None, }; @@ -889,25 +899,25 @@ mod tests { #[tokio::test] async fn test_firewall_config_comparison() { let config1 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, ipv4: true, }; let config2 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Deny, ipv4: true, }; let config3 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, ipv4: false, }; let config4 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, ipv4: true, }; @@ -927,7 +937,7 @@ mod tests { connected: Arc::new(AtomicBool::new(false)), client, stats_thread: None, - firewall_api: FirewallApi::new("test_interface"), + firewall_api: FirewallApi::new("test_interface").unwrap(), firewall_config: None, }; // Gateway has no config @@ -950,12 +960,12 @@ mod tests { let config5 = FirewallConfig { rules: vec![FirewallRule { comment: None, - destination_addrs: vec![], - destination_ports: vec![], + destination_addrs: Vec::new(), + destination_ports: Vec::new(), id: 0, verdict: Policy::Allow, - protocols: vec![], - source_addrs: vec![], + protocols: Vec::new(), + source_addrs: Vec::new(), ipv4: true, }], default_policy: Policy::Allow, diff --git a/src/main.rs b/src/main.rs index 3a129cce..c3d57d94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ async fn main() -> Result<(), GatewayError> { } let ifname = config.ifname.clone(); - let firewall_api = FirewallApi::new(&ifname); + let firewall_api = FirewallApi::new(&ifname)?; let mut gateway = if config.userspace { let wgapi = WGApi::::new(ifname)?; From f9a0d793780527d4b090dda187557638af025e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Thu, 15 May 2025 14:58:39 +0200 Subject: [PATCH 02/18] Implment PF add_rules() --- examples/server.rs | 2 +- src/enterprise/firewall/api.rs | 12 +- src/enterprise/firewall/dummy/mod.rs | 4 - src/enterprise/firewall/mod.rs | 7 +- src/enterprise/firewall/nftables/mod.rs | 177 ++++++++++-------- src/enterprise/firewall/nftables/netfilter.rs | 4 +- src/enterprise/firewall/packetfilter/calls.rs | 5 +- src/enterprise/firewall/packetfilter/mod.rs | 73 +++++++- src/enterprise/firewall/packetfilter/rule.rs | 10 +- 9 files changed, 179 insertions(+), 115 deletions(-) diff --git a/examples/server.rs b/examples/server.rs index c7250581..34a9df9f 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -107,7 +107,7 @@ impl proto::gateway::gateway_service_server::GatewayService for GatewayServer { let mut stream = request.into_inner(); while let Some(peer_stats) = stream.message().await? { - eprintln!("STATS {:?}", peer_stats); + eprintln!("STATS {peer_stats:?}"); } Ok(Response::new(())) } diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index 1bc9c391..12bc3f34 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -12,22 +12,17 @@ const DEV_PF: &str = "/dev/pf"; pub struct FirewallApi { pub(crate) ifname: String, #[cfg(any(target_os = "freebsd", target_os = "macos"))] - file: File, + pub(crate) file: File, #[cfg(target_os = "linux")] pub(crate) batch: Option, } impl FirewallApi { - #[must_use] pub fn new>(ifname: S) -> Result { Ok(Self { ifname: ifname.into(), #[cfg(any(target_os = "freebsd", target_os = "macos"))] - file: OpenOptions::new() - .read(true) - .write(true) - .open(DEV_PF) - .map_err(|err| FirewallError::Io(err))?, + file: OpenOptions::new().read(true).write(true).open(DEV_PF)?, #[cfg(target_os = "linux")] batch: None, }) @@ -42,9 +37,6 @@ pub(crate) trait FirewallManagementApi { /// Clean up the firewall rules. fn cleanup(&mut self) -> Result<(), FirewallError>; - /// Add firewall `rule`. - fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError>; - /// Add fireall `rules`. fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError>; diff --git a/src/enterprise/firewall/dummy/mod.rs b/src/enterprise/firewall/dummy/mod.rs index e3a30a2a..38d8437c 100644 --- a/src/enterprise/firewall/dummy/mod.rs +++ b/src/enterprise/firewall/dummy/mod.rs @@ -28,10 +28,6 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), FirewallError> { - Ok(()) - } - fn begin(&mut self) -> Result<(), FirewallError> { Ok(()) } diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index f5ae4951..d50d0541 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -116,10 +116,7 @@ pub(crate) enum Protocol { impl Protocol { #[must_use] pub(crate) fn supports_ports(&self) -> bool { - match self { - Protocol::Tcp | Protocol::Udp => true, - _ => false, - } + matches!(self, Protocol::Tcp | Protocol::Udp) } } @@ -276,6 +273,8 @@ pub enum FirewallError { IpAddrRange(#[from] IpAddrRangeError), #[error("Io error: {0}")] Io(#[from] std::io::Error), + #[error("Errno:{0}")] + Errno(#[from] nix::errno::Errno), #[error("Type conversion error: {0}")] TypeConversionError(String), #[error("Out of memory: {0}")] diff --git a/src/enterprise/firewall/nftables/mod.rs b/src/enterprise/firewall/nftables/mod.rs index 2dec323f..55ccca82 100644 --- a/src/enterprise/firewall/nftables/mod.rs +++ b/src/enterprise/firewall/nftables/mod.rs @@ -50,79 +50,9 @@ pub struct FilterRule<'a> { pub negated_iifname: bool, } -impl FirewallManagementApi for FirewallApi { - /// Sets up the firewall with the given default policy and priority. Drops the previous table. - /// - /// This function also begins a batch of operations which can be applied later using the [`apply`] method. - /// This allows for making atomic changes to the firewall rules. - fn setup( - &mut self, - default_policy: Policy, - priority: Option, - ) -> Result<(), FirewallError> { - debug!("Initializing firewall, VPN interface: {}", &self.ifname); - if let Some(batch) = &mut self.batch { - drop_table(batch, &self.ifname)?; - init_firewall(default_policy, priority, batch, &self.ifname) - .expect("Failed to setup chains"); - debug!("Allowing all established traffic"); - ignore_unrelated_traffic(batch, &self.ifname)?; - allow_established_traffic(batch, &self.ifname)?; - debug!("Allowed all established traffic"); - debug!("Initialized firewall"); - Ok(()) - } else { - Err(FirewallError::TransactionNotStarted) - } - } - - /// Cleans up the whole Defguard table. - fn cleanup(&mut self) -> Result<(), FirewallError> { - debug!("Cleaning up all previous firewall rules, if any"); - if let Some(batch) = &mut self.batch { - drop_table(batch, &self.ifname)?; - } else { - return Err(FirewallError::TransactionNotStarted); - } - debug!("Cleaned up all previous firewall rules"); - Ok(()) - } - - /// Allows for changing the default policy of the firewall. - fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { - debug!("Setting default firewall policy to: {policy:?}"); - if let Some(batch) = &mut self.batch { - set_default_policy(policy, batch, &self.ifname)?; - } else { - return Err(FirewallError::TransactionNotStarted); - } - debug!("Set firewall default policy to {policy:?}"); - Ok(()) - } - - /// Allows for changing the masquerade status of the firewall. - fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { - debug!("Setting masquerade status to: {enabled:?}"); - if let Some(batch) = &mut self.batch { - set_masq(&self.ifname, enabled, batch)?; - } else { - return Err(FirewallError::TransactionNotStarted); - } - debug!("Set masquerade status to: {enabled:?}"); - Ok(()) - } - - fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { - debug!("Applying the following Defguard ACL rules: {:?}", rules); - for rule in rules { - self.add_rule(rule)?; - } - debug!("Applied all Defguard ACL rules"); - Ok(()) - } - +impl FirewallApi { fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError> { - debug!("Applying the following Defguard ACL rule: {:?}", rule); + debug!("Applying the following Defguard ACL rule: {rule:?}"); let mut rules = Vec::new(); let batch = if let Some(ref mut batch) = self.batch { batch @@ -130,9 +60,15 @@ impl FirewallManagementApi for FirewallApi { return Err(FirewallError::TransactionNotStarted); }; - debug!("The rule will be split into multiple nftables rules based on the specified destination ports and protocols."); + debug!( + "The rule will be split into multiple nftables rules based on the specified \ + destination ports and protocols." + ); if rule.destination_ports.is_empty() { - debug!("No destination ports specified, applying single aggregate nftables rule for every protocol."); + debug!( + "No destination ports specified, applying single aggregate nftables rule for \ + every protocol." + ); let rule = FilterRule { src_ips: &rule.source_addrs, dest_ips: &rule.destination_addrs, @@ -146,9 +82,12 @@ impl FirewallManagementApi for FirewallApi { }; rules.push(rule); } else if !rule.protocols.is_empty() { - debug!("Destination ports and protocols specified, applying individual nftables rules for each protocol."); + debug!( + "Destination ports and protocols specified, applying individual nftables rules \ + for each protocol." + ); for protocol in rule.protocols { - debug!("Applying rule for protocol: {:?}", protocol); + debug!("Applying rule for protocol: {protocol:?}"); if protocol.supports_ports() { debug!("Protocol supports ports, rule."); let rule = FilterRule { @@ -165,7 +104,10 @@ impl FirewallManagementApi for FirewallApi { }; rules.push(rule); } else { - debug!("Protocol does not support ports, applying nftables rule and ignoring destination ports."); + debug!( + "Protocol does not support ports, applying nftables rule and ignoring \ + destination ports." + ); let rule = FilterRule { src_ips: &rule.source_addrs, dest_ips: &rule.destination_addrs, @@ -182,7 +124,8 @@ impl FirewallManagementApi for FirewallApi { } } else { debug!( - "Destination ports specified, but no protocols specified, applying nftables rules for each protocol that support ports." + "Destination ports specified, but no protocols specified, applying nftables rules \ + for each protocol that support ports." ); for protocol in [Protocol::Tcp, Protocol::Udp] { debug!("Applying nftables rule for protocol: {protocol:?}"); @@ -210,6 +153,78 @@ impl FirewallManagementApi for FirewallApi { ); Ok(()) } +} + +impl FirewallManagementApi for FirewallApi { + /// Sets up the firewall with the given default policy and priority. Drops the previous table. + /// + /// This function also begins a batch of operations which can be applied later using the [`apply`] method. + /// This allows for making atomic changes to the firewall rules. + fn setup( + &mut self, + default_policy: Policy, + priority: Option, + ) -> Result<(), FirewallError> { + debug!("Initializing firewall, VPN interface: {}", self.ifname); + if let Some(batch) = &mut self.batch { + drop_table(batch, &self.ifname)?; + init_firewall(default_policy, priority, batch, &self.ifname) + .expect("Failed to setup chains"); + debug!("Allowing all established traffic"); + ignore_unrelated_traffic(batch, &self.ifname)?; + allow_established_traffic(batch, &self.ifname)?; + debug!("Allowed all established traffic"); + debug!("Initialized firewall"); + Ok(()) + } else { + Err(FirewallError::TransactionNotStarted) + } + } + + /// Cleans up the whole Defguard table. + fn cleanup(&mut self) -> Result<(), FirewallError> { + debug!("Cleaning up all previous firewall rules, if any"); + if let Some(batch) = &mut self.batch { + drop_table(batch, &self.ifname)?; + } else { + return Err(FirewallError::TransactionNotStarted); + } + debug!("Cleaned up all previous firewall rules"); + Ok(()) + } + + /// Allows for changing the default policy of the firewall. + fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { + debug!("Setting default firewall policy to: {policy:?}"); + if let Some(batch) = &mut self.batch { + set_default_policy(policy, batch, &self.ifname)?; + } else { + return Err(FirewallError::TransactionNotStarted); + } + debug!("Set firewall default policy to {policy:?}"); + Ok(()) + } + + /// Allows for changing the masquerade status of the firewall. + fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { + debug!("Setting masquerade status to: {enabled:?}"); + if let Some(batch) = &mut self.batch { + set_masq(&self.ifname, enabled, batch)?; + } else { + return Err(FirewallError::TransactionNotStarted); + } + debug!("Set masquerade status to: {enabled:?}"); + Ok(()) + } + + fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { + debug!("Applying the following Defguard ACL rules: {rules:?}"); + for rule in rules { + self.add_rule(rule)?; + } + debug!("Applied all Defguard ACL rules"); + Ok(()) + } fn begin(&mut self) -> Result<(), FirewallError> { if self.batch.is_none() { @@ -220,8 +235,10 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } else { Err(FirewallError::TransactionFailed( - "There is another firewall transaction already in progress. Commit or rollback it before starting a new one.".to_string() - )) + "There is another firewall transaction already in progress. Commit or \ + rollback it before starting a new one." + .to_string(), + )) } } diff --git a/src/enterprise/firewall/nftables/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs index e4b8e6d1..e65ded55 100644 --- a/src/enterprise/firewall/nftables/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -21,7 +21,7 @@ use crate::enterprise::firewall::{iprange::IpAddrRange, FirewallError}; const FILTER_TABLE: &str = "filter"; const NAT_TABLE: &str = "nat"; -const DEFGUARD_TABLE: &str = "DEFGUARD-{IFNAME}"; +const DEFGUARD_TABLE: &str = "DEFGUARD-"; const POSTROUTING_CHAIN: &str = "POSTROUTING"; const FORWARD_CHAIN: &str = "FORWARD"; const ANON_SET_NAME: &CStr = c"__set%d"; @@ -712,7 +712,7 @@ impl Tables { *family, ), Self::Defguard(family) => Table::new( - &CString::new(DEFGUARD_TABLE.replace("{IFNAME}", ifname)) + &CString::new(DEFGUARD_TABLE.to_owned() + ifname) .expect("Failed to create CString from DEFGUARD_TABLE constant."), *family, ), diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index 726f36c7..9f5101f9 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -482,7 +482,7 @@ impl Rule { } } - pub fn from_rule(pf_rule: &PacketFilterRule) -> Self { + pub fn from_pf_rule(pf_rule: &PacketFilterRule) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -737,6 +737,9 @@ ioctl_readwrite!(pf_begin, b'D', 81, IocTrans); // DIOCXCOMMIT ioctl_readwrite!(pf_commit, b'D', 82, IocTrans); +// DIOCXROLLBACK +ioctl_readwrite!(pf_rollback, b'D', 83, IocTrans); + // DIOCXEND // Required by OpenBSD to release the ticket obtained by the DIOCGETRULES command. // #[cfg(target_os = "openbsd")] diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index 12f2e147..2bd7a05f 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -17,9 +17,10 @@ mod calls; mod rule; mod ticket; -use std::os::fd::AsRawFd; +use std::os::fd::{AsRawFd, RawFd}; -use ipnetwork::IpNetwork; +use calls::pf_rollback; +use rule::PacketFilterRule; use self::{ calls::{ @@ -169,6 +170,41 @@ impl PacketFilter { } */ +const ANCHOR_PREFIX: &str = "defguard"; + +impl FirewallApi { + fn anchor(&self) -> String { + ANCHOR_PREFIX.to_owned() + &self.ifname + } + + fn fd(&self) -> RawFd { + self.file.as_raw_fd() + } + + /// Add fireall `rules`. + fn add_rule( + &mut self, + rule: FirewallRule, + ticket: u32, + pool_ticket: u32, + anchor: &str, + ) -> Result<(), FirewallError> { + let rules = PacketFilterRule::from_firewall_rule(rule); + + for rule in rules { + let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); + ioc.action = Change::None; + ioc.ticket = ticket; + ioc.pool_ticket = pool_ticket; + unsafe { + pf_add_rule(self.fd(), &mut ioc)?; + } + } + + Ok(()) + } +} + #[cfg(not(test))] impl FirewallManagementApi for FirewallApi { fn setup( @@ -184,13 +220,30 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - /// Add firewall `rule`. - fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError> { - Ok(()) - } - /// Add fireall `rules`. fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { + let anchor = &self.anchor(); + // Begin transaction. + let element = IocTransElement::new(RuleSet::Filter, anchor); + let mut elements = [element]; + let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); + // This will create an anchor if it doesn't exist. + unsafe { + pf_begin(self.fd(), &mut ioc_trans)?; + } + + let ticket = elements[0].ticket; + let pool_ticket = get_pool_ticket(self.fd(), anchor); + + for rule in rules { + if let Err(err) = self.add_rule(rule, ticket, pool_ticket, anchor) { + unsafe { + pf_rollback(self.fd(), &mut ioc_trans)?; + return Err(FirewallError::TransactionFailed(err.to_string())); + } + } + } + Ok(()) } @@ -206,14 +259,18 @@ impl FirewallManagementApi for FirewallApi { /// Begin rule transaction. fn begin(&mut self) -> Result<(), FirewallError> { + // TODO: remove this no-op. Ok(()) } /// Commit rule transaction. fn commit(&mut self) -> Result<(), FirewallError> { + // TODO: remove this no-op. Ok(()) } /// Rollback rule transaction. - fn rollback(&mut self) {} + fn rollback(&mut self) { + // TODO: remove this no-op. + } } diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index d37010c4..f46dac46 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -150,6 +150,7 @@ impl PacketFilterRule { } } + /// Expand `FirewallRule` into a set of `PacketFilterRule`s. pub(crate) fn from_firewall_rule(mut fr: FirewallRule) -> Vec { let mut rules = Vec::new(); let action = match fr.verdict { @@ -197,16 +198,15 @@ impl PacketFilterRule { fr.protocols.push(Protocol::Any); } - // Unwrap `FirewallRule` converting it to a set of `PacketFilterRule`s. for from in &from_addrs { for to in &to_addrs { for to_port in &fr.destination_ports { for proto in &fr.protocols { let rule = Self { - from: from.clone(), + from: *from, from_port: Port::Any, - to: to.clone(), - to_port: to_port.clone(), + to: *to, + to_port: *to_port, action, direction: Direction::InOut, quick: false, @@ -214,7 +214,7 @@ impl PacketFilterRule { log: 0, keep_state: State::Normal, interface: None, - proto: proto.clone(), + proto: *proto, // For stateful connections, the default is flags S/SA. tcp_flags: TH_SYN, tcp_flags_set: TH_SYN | TH_ACK, From eb3a878135ee9a576d044c3d469d4c406afc409c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Thu, 15 May 2025 15:03:00 +0200 Subject: [PATCH 03/18] Use ifname for PF rules --- src/enterprise/firewall/packetfilter/mod.rs | 2 +- src/enterprise/firewall/packetfilter/rule.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index 2bd7a05f..291dce68 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -189,7 +189,7 @@ impl FirewallApi { pool_ticket: u32, anchor: &str, ) -> Result<(), FirewallError> { - let rules = PacketFilterRule::from_firewall_rule(rule); + let rules = PacketFilterRule::from_firewall_rule(&self.ifname, rule); for rule in rules { let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index f46dac46..fad9ac24 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -151,7 +151,7 @@ impl PacketFilterRule { } /// Expand `FirewallRule` into a set of `PacketFilterRule`s. - pub(crate) fn from_firewall_rule(mut fr: FirewallRule) -> Vec { + pub(crate) fn from_firewall_rule(ifname: &str, mut fr: FirewallRule) -> Vec { let mut rules = Vec::new(); let action = match fr.verdict { Policy::Allow => Action::Pass, @@ -213,7 +213,7 @@ impl PacketFilterRule { // Disable logging. log: 0, keep_state: State::Normal, - interface: None, + interface: Some(ifname.to_owned()), proto: *proto, // For stateful connections, the default is flags S/SA. tcp_flags: TH_SYN, @@ -250,7 +250,7 @@ mod tests { ipv4: true, }; - let rules = PacketFilterRule::from_firewall_rule(fr); + let rules = PacketFilterRule::from_firewall_rule("lo0", fr); assert_eq!(1, rules.len()); // One address, one port. @@ -268,7 +268,7 @@ mod tests { ipv4: true, }; - let rules = PacketFilterRule::from_firewall_rule(fr); + let rules = PacketFilterRule::from_firewall_rule("lo0", fr); assert_eq!(1, rules.len()); // Two addresses, two ports. @@ -289,7 +289,7 @@ mod tests { ipv4: true, }; - let rules = PacketFilterRule::from_firewall_rule(fr); + let rules = PacketFilterRule::from_firewall_rule("lo0", fr); assert_eq!(4, rules.len()); } } From a7775bc5eecf88af1ef3d9ffa69f527373568308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 16 May 2025 13:05:10 +0200 Subject: [PATCH 04/18] Adding rules works --- src/enterprise/firewall/packetfilter/calls.rs | 38 ++--- src/enterprise/firewall/packetfilter/mod.rs | 150 ++---------------- src/enterprise/firewall/packetfilter/rule.rs | 4 +- src/gateway.rs | 47 +++--- 4 files changed, 46 insertions(+), 193 deletions(-) diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index 9f5101f9..e8f4bd95 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -306,12 +306,6 @@ pub struct pf_anchor { pub owner: [c_char; 64], } -/// A packed Operating System description for fingerprinting. -type pf_osfp_t = c_uint; -// #define PF_OSFP_ANY ((pf_osfp_t)0) -// #define PF_OSFP_UNKNOWN ((pf_osfp_t)-1) -// #define PF_OSFP_NOMATCH ((pf_osfp_t)-2) - #[derive(Debug)] #[repr(C)] pub struct pf_rule_conn_rate { @@ -364,7 +358,7 @@ pub struct Rule { pub anchor: *mut pf_anchor, pub overload_tbl: *mut c_void, // struct pfr_ktable, kernel only - pub os_fingerprint: pf_osfp_t, + pub os_fingerprint: c_uint, pub rtableid: c_uint, #[cfg(target_os = "freebsd")] @@ -462,26 +456,6 @@ pub struct Rule { } impl Rule { - // TODO: expand - #[must_use] - pub fn new(src: IpNetwork, src_port: Port) -> Self { - let mut uninit = MaybeUninit::::zeroed(); - let self_ptr = uninit.as_mut_ptr(); - - unsafe { - (*self_ptr).dst = RuleAddr::new(src, src_port); - // Set address family. - // TODO: match empty network, then set AF_UNSPEC. - // (*self_ptr).af = match src { - // IpNetwork::V4(_) => AF_INET as u8, - // IpNetwork::V6(_) => AF_INET6 as u8, - // }; - (*self_ptr).keep_state = State::Normal; - - uninit.assume_init() - } - } - pub fn from_pf_rule(pf_rule: &PacketFilterRule) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -493,14 +467,24 @@ impl Rule { if let Some(to) = pf_rule.to { (*self_ptr).dst = RuleAddr::new(to, pf_rule.to_port); } + if let Some(interface) = &pf_rule.interface { + let len = interface.len().min(IFNAMSIZ - 1); + (*self_ptr).ifname[..len].copy_from_slice(&interface.as_bytes()[..len]); + } if let Some(label) = &pf_rule.label { let len = label.len().min(PF_RULE_LABEL_SIZE - 1); (*self_ptr).label[..len].copy_from_slice(&label.as_bytes()[..len]); } (*self_ptr).action = pf_rule.action; (*self_ptr).direction = pf_rule.direction; + (*self_ptr).log = pf_rule.log; (*self_ptr).quick = pf_rule.quick; + + (*self_ptr).keep_state = pf_rule.keep_state; (*self_ptr).af = pf_rule.address_family(); + (*self_ptr).proto = pf_rule.proto as u8; + (*self_ptr).flags = pf_rule.tcp_flags; + (*self_ptr).flagset = pf_rule.tcp_flags_set; uninit.assume_init() } diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index 291dce68..b91d090e 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -23,12 +23,9 @@ use calls::pf_rollback; use rule::PacketFilterRule; use self::{ - calls::{ - pf_add_rule, pf_begin, pf_change_rule, pf_commit, Change, IocRule, IocTrans, - IocTransElement, Rule, - }, - rule::{Action, Direction, RuleSet}, - ticket::{get_pool_ticket, get_ticket}, + calls::{pf_add_rule, pf_begin, pf_commit, Change, IocRule, IocTrans, IocTransElement, Rule}, + rule::RuleSet, + ticket::get_pool_ticket, }; use crate::enterprise::firewall::Port; @@ -37,140 +34,7 @@ use super::{ FirewallError, FirewallRule, Policy, }; -/* -impl PacketFilter { - pub fn new() -> std::io::Result { - let file = OpenOptions::new().read(true).write(true).open(DEV_PF)?; - Ok(Self { file }) - } - - pub fn enable(&self) { - unsafe { - calls::pf_start(self.file.as_raw_fd()).unwrap(); - } - } - - /// Return ticket for filter rules. - pub fn begin(&self, anchor: &str) -> u32 { - let element = IocTransElement::new(RuleSet::Filter, anchor); - // let mut elements = vec![element]; - let mut elements = [element]; - let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); - - // This will create an anchor. - unsafe { - pf_begin(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); - } - - elements[0].ticket - } - - // TODO: expand - pub fn add_rule(&self, src: IpNetwork, src_port: Port, anchor: &str) { - // let ticket = self.begin(anchor); - - let element = IocTransElement::new(RuleSet::Filter, anchor); - // let mut elements = vec![element]; - let mut elements = [element]; - let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); - - // This will create an anchor. - unsafe { - pf_begin(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); - } - - let ticket = elements[0].ticket; - - // --- - let pool_ticket = get_pool_ticket(self.file.as_raw_fd(), anchor); - - let mut rule = Rule::new(src, src_port); - // rule.action = Change::AddTail; FreeBSD/OpenBSD only? - rule.direction = Direction::In; - - // eprintln!("Src {:?}", rule.src); - // eprintln!("Dst {:?}", rule.dst); - eprintln!("{:?}", rule); - - let mut ioc = IocRule::with_rule(anchor, rule); - ioc.action = Change::None; - ioc.ticket = ticket; - ioc.pool_ticket = pool_ticket; - - // pf_add_rule returns EBUSY on macOS. - unsafe { - pf_add_rule(self.file.as_raw_fd(), &mut ioc).unwrap(); - pf_commit(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); - } - } - - // TODO: expand - pub fn append_rule(&self, src: IpNetwork, src_port: Port, anchor: &str) { - // OpenBSD has no pool tickets - // #[cfg(any(target_os = "macos", target_os = "freebsd"))] - let pool_ticket = get_pool_ticket(self.file.as_raw_fd(), anchor); - let ticket = get_ticket(self.file.as_raw_fd(), anchor, Action::Pass); - - eprintln!("Ticket {ticket}, pool ticket {pool_ticket}"); - - let mut rule = Rule::new(src, src_port); - // rule.action = Change::AddTail; FreeBSD/OpenBSD only? - rule.direction = Direction::In; - - let mut ioc = IocRule::with_rule(anchor, rule); - ioc.action = Change::AddHead; - ioc.ticket = ticket; - ioc.pool_ticket = pool_ticket; - - // pf_add_rule returns EBUSY on macOS. - unsafe { - // pf_add_rule(self.file.as_raw_fd(), &mut ioc).unwrap(); - pf_change_rule(self.file.as_raw_fd(), &mut ioc).unwrap(); - } - } - - // Add anchor with a given `name`. - // FIXME: This method only works on macOS. - // pub fn add_anchor(&self, name: &str) { - // // #[cfg(target_os = "macos")] - // // { - // // pfioc_rule.rule.action = kind.into(); - // // } - // // #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] - // // { - // // pfioc_rule.rule.action = PF_CHANGE_REMOVE as u8; - // // } - - // // FIXME: empty - // let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); - // let mut rule = Rule::new(src); - - // #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] - // { - // rule.action = Action::NoNat; // = Change::Remove - // } - - // let mut ioc = IocRule { - // action: Change::None, - // ticket: 0, - // pool_ticket: 0, - // nr: 0, - // anchor: [0; 1024], - // anchor_call: [0; 1024], - // rule, - // }; - // name.bytes() - // .take(1023) - // .enumerate() - // .for_each(|(i, b)| ioc.anchor_call[i] = b); - - // // unsafe { pf_insert_rule(self.file.as_raw_fd(), &mut ioc).unwrap() }; - // unsafe { pf_change_rule(self.file.as_raw_fd(), &mut ioc).unwrap() }; - // } -} -*/ - -const ANCHOR_PREFIX: &str = "defguard"; +const ANCHOR_PREFIX: &str = "defguard."; impl FirewallApi { fn anchor(&self) -> String { @@ -189,7 +53,9 @@ impl FirewallApi { pool_ticket: u32, anchor: &str, ) -> Result<(), FirewallError> { + warn!("add_rule {rule:?}"); let rules = PacketFilterRule::from_firewall_rule(&self.ifname, rule); + warn!("--> rules {rules:?}"); for rule in rules { let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); @@ -244,6 +110,10 @@ impl FirewallManagementApi for FirewallApi { } } + unsafe { + pf_commit(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); + } + Ok(()) } diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index fad9ac24..d7bdac7c 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -141,9 +141,9 @@ pub struct PacketFilterRule { } impl PacketFilterRule { - /// Determine address family based on `from` field. + /// Determine address family based on `to` field. pub(crate) fn address_family(&self) -> AddressFamily { - match self.from { + match self.to { None => AddressFamily::Unspec, Some(IpNetwork::V4(_)) => AddressFamily::Inet, Some(IpNetwork::V6(_)) => AddressFamily::Inet6, diff --git a/src/gateway.rs b/src/gateway.rs index 97db03af..3117fecf 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -26,13 +26,12 @@ use tonic::{ Request, Status, Streaming, }; -#[cfg(target_os = "linux")] -use crate::enterprise::firewall::api::FirewallManagementApi; -#[cfg(any(target_os = "linux", test))] -use crate::enterprise::firewall::FirewallRule; use crate::{ config::Config, - enterprise::firewall::{api::FirewallApi, FirewallConfig}, + enterprise::firewall::{ + api::{FirewallApi, FirewallManagementApi}, + FirewallConfig, FirewallRule, + }, error::GatewayError, execute_command, mask, proto::gateway::{ @@ -260,7 +259,6 @@ impl Gateway { } /// Checks whether the firewall config changed - #[cfg(any(target_os = "linux", test))] fn has_firewall_config_changed(&self, new_fw_config: &FirewallConfig) -> bool { if let Some(current_config) = &self.firewall_config { return current_config.default_policy != new_fw_config.default_policy @@ -271,7 +269,6 @@ impl Gateway { } /// Checks whether the firewall rules have changed. - #[cfg(any(target_os = "linux", test))] fn have_firewall_rules_changed(&self, new_rules: &[FirewallRule]) -> bool { debug!("Checking if Defguard ACL rules have changed"); if let Some(current_config) = &self.firewall_config { @@ -309,7 +306,6 @@ impl Gateway { /// should be temporary. /// /// TODO: Reduce cloning here - #[cfg(target_os = "linux")] fn process_firewall_changes( &mut self, fw_config: Option<&FirewallConfig>, @@ -386,17 +382,14 @@ impl Gateway { debug!("Received configuration is identical to the current one. Skipping interface reconfiguration."); } - #[cfg(target_os = "linux")] - { - let new_firewall_configuration = - if let Some(firewall_config) = new_configuration.firewall_config { - Some(FirewallConfig::from_proto(firewall_config)?) - } else { - None - }; - - self.process_firewall_changes(new_firewall_configuration.as_ref())?; - } + let new_firewall_configuration = + if let Some(firewall_config) = new_configuration.firewall_config { + Some(FirewallConfig::from_proto(firewall_config)?) + } else { + None + }; + + self.process_firewall_changes(new_firewall_configuration.as_ref())?; Ok(()) } @@ -517,27 +510,33 @@ impl Gateway { } }; } - #[cfg(target_os = "linux")] Some(update::Update::FirewallConfig(config)) => { debug!("Applying received firewall configuration: {config:?}"); let config_str = format!("{:?}", config); match FirewallConfig::from_proto(config) { Ok(new_firewall_config) => { - debug!("Parsed the received firewall configuration: {new_firewall_config:?}, processing it and applying changes"); + debug!( + "Parsed the received firewall configuration: \ + {new_firewall_config:?}, processing it and applying \ + changes" + ); if let Err(err) = self.process_firewall_changes(Some(&new_firewall_config)) { - error!("Failed to process received firewall configuration: {err}"); + error!( + "Failed to process received firewall configuration: \ + {err}" + ); } } Err(err) => { error!( - "Failed to parse received firewall configuration: {err}. Configuration: {config_str}" + "Failed to parse received firewall configuration: {err}. \ + Configuration: {config_str}" ); } } } - #[cfg(target_os = "linux")] Some(update::Update::DisableFirewall(_)) => { debug!("Disabling firewall configuration"); if let Err(err) = self.process_firewall_changes(None) { From 79869ff568dd8b7e5f3e270fa29bcd87cae70ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Wed, 21 May 2025 12:00:30 +0200 Subject: [PATCH 05/18] Cleanup --- Cargo.lock | 32 +- src/enterprise/firewall/iprange.rs | 48 ++- src/enterprise/firewall/packetfilter/calls.rs | 367 +++++++++--------- 3 files changed, 211 insertions(+), 236 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03b20791..19464de7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,9 +263,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "byteorder" @@ -281,9 +281,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.22" +version = "1.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" dependencies = [ "jobserver", "libc", @@ -581,9 +581,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -853,9 +853,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" dependencies = [ "bytes", "futures-channel", @@ -920,9 +920,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -936,9 +936,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -1033,9 +1033,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -1046,9 +1046,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", diff --git a/src/enterprise/firewall/iprange.rs b/src/enterprise/firewall/iprange.rs index 722099b2..aa793043 100644 --- a/src/enterprise/firewall/iprange.rs +++ b/src/enterprise/firewall/iprange.rs @@ -1,13 +1,19 @@ +//! Range of IP addresses. +//! +//! Encapsulates a range of IP addresses, which can be iterated. +//! For the time being, `RangeInclusive` can't be used, because `IpAddr` does not implement +//! `Step` trait. + use std::{ fmt, net::{IpAddr, Ipv4Addr, Ipv6Addr}, - ops::Range, + ops::RangeInclusive, }; #[derive(Clone, Debug, PartialEq)] pub enum IpAddrRange { - V4(Range), - V6(Range), + V4(RangeInclusive), + V6(RangeInclusive), } #[derive(Debug, thiserror::Error)] @@ -31,8 +37,8 @@ impl IpAddrRange { Err(IpAddrRangeError::WrongOrder) } else { match (start, end) { - (IpAddr::V4(start), IpAddr::V4(end)) => Ok(Self::V4(Range { start, end })), - (IpAddr::V6(start), IpAddr::V6(end)) => Ok(Self::V6(Range { start, end })), + (IpAddr::V4(start), IpAddr::V4(end)) => Ok(Self::V4(start..=end)), + (IpAddr::V6(start), IpAddr::V6(end)) => Ok(Self::V6(start..=end)), _ => Err(IpAddrRangeError::MixedTypes), } } @@ -90,43 +96,33 @@ mod tests { fn test_range() { let start = IpAddr::V4(Ipv4Addr::LOCALHOST); let end = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)); - let range = Range { start, end }; + let range = start..=end; - let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); - assert!(range.contains(&a)); + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + assert!(range.contains(&addr)); - let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); - assert!(!range.contains(&a)); + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); + assert!(!range.contains(&addr)); - // As of Rust 1.86, `IpAddr` does not implement `Step`. + // As of Rust 1.87.0, `IpAddr` does not implement `Step`. // assert_eq!(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), range.next()); } - #[test] - fn test_v4() { - let start = Ipv4Addr::LOCALHOST; - let end = Ipv4Addr::new(127, 0, 0, 3); - let mut range = Range { start, end }; - - assert_eq!(Some(Ipv4Addr::new(127, 0, 0, 1)), range.next()); - assert_eq!(Some(Ipv4Addr::new(127, 0, 0, 2)), range.next()); - assert_eq!(None, range.next()); - } - #[test] fn test_ipaddrrange() { let start = IpAddr::V4(Ipv4Addr::LOCALHOST); let end = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)); let mut range = IpAddrRange::new(start, end).unwrap(); - let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); - assert!(range.contains(&a)); + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + assert!(range.contains(&addr)); - let a = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); - assert!(!range.contains(&a)); + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); + assert!(!range.contains(&addr)); assert_eq!(Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), range.next()); assert_eq!(Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2))), range.next()); + assert_eq!(Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3))), range.next()); assert_eq!(None, range.next()); } } diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index e8f4bd95..e2dab019 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -1,3 +1,5 @@ +//! Low level communication with Packet Filter. + use std::{ ffi::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong, c_ushort, c_void}, mem::{size_of, MaybeUninit}, @@ -15,10 +17,6 @@ type Addr = [u8; 16]; // Do not use u128 for the sake of alignment. /// Equivalent to `pf_poolhashkey`: 128-bit hash key. type PoolHashKey = [u8; 16]; -const PF_TABLE_NAME_SIZE: usize = 32; -#[cfg(target_os = "macos")] -const RTLABEL_LEN: usize = 32; - /// Equivalent to `struct pf_addr_wrap_addr_mask`. #[derive(Debug)] #[repr(C)] @@ -54,7 +52,7 @@ impl From for AddrMask { /// Only the `v` part of the union, as `p` is not used in this crate. #[derive(Debug)] #[repr(C)] -pub struct AddrWrap { +struct AddrWrap { v: AddrMask, // pub v: pf_addr_wrap_v, // unused in this crate @@ -99,20 +97,20 @@ impl AddrWrap { /// Equivalent to `struct pf_rule_addr`. #[derive(Debug)] #[repr(C)] -pub struct RuleAddr { - pub addr: AddrWrap, +pub(super) struct RuleAddr { + addr: AddrWrap, // macOS: here `union pf_rule_xport` is flattened to its first variant: `struct pf_port_range`. - pub port: [c_ushort; 2], - pub op: PortOp, + port: [c_ushort; 2], + op: PortOp, #[cfg(target_os = "macos")] _padding: [c_uchar; 3], #[cfg(target_os = "macos")] - pub neg: c_uchar, + neg: c_uchar, } impl RuleAddr { #[must_use] - pub fn new(ip_network: IpNetwork, port: Port) -> Self { + pub(super) fn new(ip_network: IpNetwork, port: Port) -> Self { let addr = AddrWrap::new(ip_network); let from_port; let to_port; @@ -149,15 +147,16 @@ impl RuleAddr { /// TAILQ_ENTRY #[derive(Debug)] #[repr(C)] -pub struct pf_rule_list { - pub tqe_next: *mut Rule, - pub tqe_prev: *mut *mut Rule, +struct pf_rule_list { + tqe_next: *mut Rule, + tqe_prev: *mut *mut Rule, } +#[derive(Debug)] #[repr(C)] -pub struct pf_pooladdr_list { - pub tqe_next: *mut PoolAddr, - pub tqe_prev: *mut *mut PoolAddr, +struct pf_pooladdr_list { + tqe_next: *mut PoolAddr, + tqe_prev: *mut *mut PoolAddr, } // Equivalent to `struct pf_pooladdr`. @@ -184,33 +183,26 @@ impl PoolAddr { } } -#[derive(Debug)] -#[repr(C)] -pub struct pf_palist { - pub tqh_first: *mut PoolAddr, - pub tqh_last: *mut *mut PoolAddr, -} - /// Equivalent to `struct pf_pool`. #[derive(Debug)] #[repr(C)] -pub struct Pool { - pub list: pf_palist, - pub cur: *mut c_void, - pub key: PoolHashKey, - pub counter: Addr, - pub tblidx: c_int, - pub proxy_port: [c_ushort; 2], +pub(super) struct Pool { + list: pf_pooladdr_list, + cur: *mut c_void, + key: PoolHashKey, + counter: Addr, + tblidx: c_int, + proxy_port: [c_ushort; 2], #[cfg(target_os = "macos")] - pub port_op: PortOp, - pub opts: c_uchar, + port_op: PortOp, + opts: c_uchar, #[cfg(target_os = "macos")] - pub af: sa_family_t, + af: sa_family_t, } #[derive(Debug)] #[repr(u8)] -pub enum PortOp { +enum PortOp { /// PF_OP_NONE = 0 None, /// PF_OP_IRG = 1 @@ -247,78 +239,78 @@ impl Pool { } #[repr(C)] -pub struct pf_anchor_global { - pub rbe_left: *mut pf_anchor, - pub rbe_right: *mut pf_anchor, - pub rbe_parent: *mut pf_anchor, +struct pf_anchor_global { + rbe_left: *mut pf_anchor, + rbe_right: *mut pf_anchor, + rbe_parent: *mut pf_anchor, } #[repr(C)] -pub struct pf_anchor_node { - pub rbe_left: *mut pf_anchor, - pub rbe_right: *mut pf_anchor, - pub rbe_parent: *mut pf_anchor, +struct pf_anchor_node { + rbe_left: *mut pf_anchor, + rbe_right: *mut pf_anchor, + rbe_parent: *mut pf_anchor, } #[repr(C)] -pub struct pf_rulequeue { - pub tqh_first: *mut Rule, - pub tqh_last: *mut *mut Rule, +struct pf_rulequeue { + tqh_first: *mut Rule, + tqh_last: *mut *mut Rule, } #[repr(C)] -pub struct pf_ruleset_rule { - pub ptr: *mut pf_rulequeue, - pub ptr_array: *mut *mut Rule, - pub rcount: c_uint, - pub rsize: c_uint, - pub ticket: c_uint, - pub open: c_int, +struct pf_ruleset_rule { + ptr: *mut pf_rulequeue, + ptr_array: *mut *mut Rule, + rcount: c_uint, + rsize: c_uint, + ticket: c_uint, + open: c_int, } #[repr(C)] -pub struct pf_ruleset_rules { - pub queues: [pf_rulequeue; 2], - pub active: pf_ruleset_rule, - pub inactive: pf_ruleset_rule, +struct pf_ruleset_rules { + queues: [pf_rulequeue; 2], + active: pf_ruleset_rule, + inactive: pf_ruleset_rule, } #[repr(C)] -pub struct pf_ruleset { - pub rules: [pf_ruleset_rules; 6], - pub anchor: *mut pf_anchor, - pub tticket: c_uint, - pub tables: c_int, - pub topen: c_int, +struct pf_ruleset { + rules: [pf_ruleset_rules; 6], + anchor: *mut pf_anchor, + tticket: c_uint, + tables: c_int, + topen: c_int, } #[repr(C)] -pub struct pf_anchor { - pub entry_global: pf_anchor_global, - pub entry_node: pf_anchor_node, - pub parent: *mut pf_anchor, - pub children: pf_anchor_node, - pub name: [c_char; 64], - pub path: [c_char; 1024], - pub ruleset: pf_ruleset, - pub refcnt: c_int, - pub match_: c_int, - pub owner: [c_char; 64], +struct pf_anchor { + entry_global: pf_anchor_global, + entry_node: pf_anchor_node, + parent: *mut pf_anchor, + children: pf_anchor_node, + name: [c_char; 64], + path: [c_char; MAXPATHLEN], + ruleset: pf_ruleset, + refcnt: c_int, + match_: c_int, + owner: [c_char; 64], } #[derive(Debug)] #[repr(C)] -pub struct pf_rule_conn_rate { - pub limit: c_uint, - pub seconds: c_uint, +struct pf_rule_conn_rate { + limit: c_uint, + seconds: c_uint, } #[derive(Debug)] #[repr(C)] -pub struct pf_rule_id { - pub uid: [uid_t; 2], - pub op: c_uchar, - // pub _pad: [u_int8_t; 3], +struct pf_rule_id { + uid: [uid_t; 2], + op: c_uchar, + //_pad: [u_int8_t; 3], } /// As defined in `net/pfvar.h`. @@ -327,136 +319,136 @@ const PF_RULE_LABEL_SIZE: usize = 64; /// Equivalent to 'struct pf_rule'. #[derive(Debug)] #[repr(C)] -pub struct Rule { +pub(super) struct Rule { src: RuleAddr, - pub dst: RuleAddr, + dst: RuleAddr, - pub skip: [usize; 8], - pub label: [c_uchar; PF_RULE_LABEL_SIZE], - pub ifname: [c_uchar; IFNAMSIZ], - pub qname: [c_uchar; 64], - pub pqname: [c_uchar; 64], - pub tagname: [c_uchar; 64], - pub match_tagname: [c_uchar; 64], - pub overload_tblname: [c_uchar; 32], + skip: [usize; 8], + label: [c_uchar; PF_RULE_LABEL_SIZE], + ifname: [c_uchar; IFNAMSIZ], + qname: [c_uchar; 64], + pqname: [c_uchar; 64], + tagname: [c_uchar; 64], + match_tagname: [c_uchar; 64], + overload_tblname: [c_uchar; 32], - pub entries: pf_rule_list, - pub rpool: Pool, + entries: pf_rule_list, + rpool: Pool, - pub evaluations: c_long, - pub packets: [c_ulong; 2], - pub bytes: [c_ulong; 2], + evaluations: c_long, + packets: [c_ulong; 2], + bytes: [c_ulong; 2], #[cfg(target_os = "macos")] - pub ticket: c_ulong, + ticket: c_ulong, #[cfg(target_os = "macos")] - pub owner: [c_char; 64], + owner: [c_char; 64], #[cfg(target_os = "macos")] - pub priority: c_int, + priority: c_int, - pub kif: *mut c_void, // struct pfi_kif, kernel only - pub anchor: *mut pf_anchor, - pub overload_tbl: *mut c_void, // struct pfr_ktable, kernel only + kif: *mut c_void, // struct pfi_kif, kernel only + anchor: *mut pf_anchor, + overload_tbl: *mut c_void, // struct pfr_ktable, kernel only - pub os_fingerprint: c_uint, + os_fingerprint: c_uint, - pub rtableid: c_uint, + rtableid: c_uint, #[cfg(target_os = "freebsd")] - pub timeout: [c_uint; 20], + timeout: [c_uint; 20], #[cfg(target_os = "macos")] - pub timeout: [c_uint; 26], + timeout: [c_uint; 26], #[cfg(target_os = "macos")] - pub states: c_uint, - pub max_states: c_uint, + states: c_uint, + max_states: c_uint, #[cfg(target_os = "macos")] - pub src_nodes: c_uint, - pub max_src_nodes: c_uint, - pub max_src_states: c_uint, - pub max_src_conn: c_uint, - pub max_src_conn_rate: pf_rule_conn_rate, - pub qid: c_uint, - pub pqid: c_uint, - pub rt_listid: c_uint, - pub nr: c_uint, - pub prob: c_uint, - pub cuid: uid_t, - pub cpid: pid_t, + src_nodes: c_uint, + max_src_nodes: c_uint, + max_src_states: c_uint, + max_src_conn: c_uint, + max_src_conn_rate: pf_rule_conn_rate, + qid: c_uint, + pqid: c_uint, + rt_listid: c_uint, + nr: c_uint, + prob: c_uint, + cuid: uid_t, + cpid: pid_t, #[cfg(target_os = "freebsd")] - pub states_cur: u64, + states_cur: u64, #[cfg(target_os = "freebsd")] - pub states_tot: u64, + states_tot: u64, #[cfg(target_os = "freebsd")] - pub src_nodes: u64, + src_nodes: u64, - pub return_icmp: c_ushort, - pub return_icmp6: c_ushort, - pub max_mss: c_ushort, - pub tag: c_ushort, - pub match_tag: c_ushort, + return_icmp: c_ushort, + return_icmp6: c_ushort, + max_mss: c_ushort, + tag: c_ushort, + match_tag: c_ushort, #[cfg(target_os = "freebsd")] - pub scrub_flags: c_ushort, - - pub uid: pf_rule_id, - pub gid: pf_rule_id, - - pub rule_flag: c_uint, // RuleFlag - pub action: Action, - pub direction: Direction, - pub log: c_uchar, // LogFlags - pub logif: c_uchar, - pub quick: bool, - pub ifnot: c_uchar, - pub match_tag_not: c_uchar, - pub natpass: c_uchar, - - pub keep_state: State, - pub af: AddressFamily, // sa_family_t - pub proto: c_uchar, - pub(crate) r#type: c_uchar, - pub code: c_uchar, - pub flags: c_uchar, // TCP_FLAG - pub flagset: c_uchar, // TCP_FLAG - pub min_ttl: c_uchar, - pub allow_opts: c_uchar, - pub rt: c_uchar, - pub return_ttl: c_uchar, - - pub tos: c_uchar, + scrub_flags: c_ushort, + + uid: pf_rule_id, + gid: pf_rule_id, + + rule_flag: c_uint, // RuleFlag + pub(super) action: Action, + direction: Direction, + log: c_uchar, // LogFlags + logif: c_uchar, + quick: bool, + ifnot: c_uchar, + match_tag_not: c_uchar, + natpass: c_uchar, + + keep_state: State, + af: AddressFamily, // sa_family_t + proto: c_uchar, + r#type: c_uchar, + code: c_uchar, + flags: c_uchar, // TCP_FLAG + flagset: c_uchar, // TCP_FLAG + min_ttl: c_uchar, + allow_opts: c_uchar, + rt: c_uchar, + return_ttl: c_uchar, + + tos: c_uchar, #[cfg(target_os = "freebsd")] - pub set_tos: c_uchar, - pub anchor_relative: c_uchar, - pub anchor_wildcard: c_uchar, - pub flush: c_uchar, + set_tos: c_uchar, + anchor_relative: c_uchar, + anchor_wildcard: c_uchar, + flush: c_uchar, #[cfg(target_os = "freebsd")] - pub prio: c_uchar, + prio: c_uchar, #[cfg(target_os = "freebsd")] - pub set_prio: [c_uchar; 2], + set_prio: [c_uchar; 2], #[cfg(target_os = "freebsd")] - pub divert: (pf_addr, u16), + divert: (pf_addr, u16), #[cfg(target_os = "freebsd")] - pub u_states_cur: u64, + u_states_cur: u64, #[cfg(target_os = "freebsd")] - pub u_states_tot: u64, + u_states_tot: u64, #[cfg(target_os = "freebsd")] - pub u_src_nodes: u64, + u_src_nodes: u64, #[cfg(target_os = "macos")] - pub proto_variant: c_uchar, + proto_variant: c_uchar, #[cfg(target_os = "macos")] - pub extfilter: c_uchar, + extfilter: c_uchar, #[cfg(target_os = "macos")] - pub extmap: c_uchar, + extmap: c_uchar, #[cfg(target_os = "macos")] - pub dnpipe: c_uint, + dnpipe: c_uint, #[cfg(target_os = "macos")] - pub dntype: c_uint, + dntype: c_uint, } impl Rule { - pub fn from_pf_rule(pf_rule: &PacketFilterRule) -> Self { + pub(super) fn from_pf_rule(pf_rule: &PacketFilterRule) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -529,7 +521,7 @@ pub(crate) const MAXPATHLEN: usize = libc::PATH_MAX as usize; /// Equivalent to `struct pfioc_rule`. #[repr(C)] -pub struct IocRule { +pub(super) struct IocRule { pub action: Change, pub ticket: c_uint, pub pool_ticket: c_uint, @@ -541,7 +533,7 @@ pub struct IocRule { impl IocRule { #[must_use] - pub fn new(anchor: &str) -> Self { + pub(super) fn new(anchor: &str) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -555,7 +547,7 @@ impl IocRule { } #[must_use] - pub fn with_rule(anchor: &str, rule: Rule) -> Self { + pub(super) fn with_rule(anchor: &str, rule: Rule) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -572,7 +564,7 @@ impl IocRule { /// Equivalent to `struct pfioc_pooladdr`. #[repr(C)] -pub struct IocPoolAddr { +pub(super) struct IocPoolAddr { action: Change, pub ticket: c_uint, nr: c_uint, @@ -598,15 +590,11 @@ impl IocPoolAddr { unsafe { uninit.assume_init() } } - - // pub fn set_addr(&mut self, addr:) { - - // } } /// Equivalent to `struct pfioc_trans_pfioc_trans_e`. #[repr(C)] -pub struct IocTransElement { +pub(super) struct IocTransElement { rs_num: RuleSet, anchor: [c_uchar; MAXPATHLEN], pub ticket: c_uint, @@ -614,7 +602,7 @@ pub struct IocTransElement { impl IocTransElement { #[must_use] - pub fn new(ruleset: RuleSet, anchor: &str) -> Self { + pub(super) fn new(ruleset: RuleSet, anchor: &str) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -631,7 +619,7 @@ impl IocTransElement { /// Equivalent to `struct pfioc_trans`. #[repr(C)] -pub struct IocTrans { +pub(super) struct IocTrans { /// number of elements size: c_int, /// size of each element in bytes @@ -641,7 +629,7 @@ pub struct IocTrans { impl IocTrans { #[must_use] - pub fn new(elements: &mut [IocTransElement]) -> Self { + pub(super) fn new(elements: &mut [IocTransElement]) -> Self { Self { size: elements.len() as i32, esize: size_of::() as i32, @@ -650,15 +638,6 @@ impl IocTrans { } } -// fn setup_trans( -// pfioc_trans: &mut pfioc_trans, -// pfioc_trans_elements: &mut [ffi::pfvar::pfioc_trans_pfioc_trans_e], -// ) { -// pfioc_trans.size = pfioc_trans_elements.len() as i32; -// pfioc_trans.esize = size_of::() as i32; -// pfioc_trans.array = pfioc_trans_elements.as_mut_ptr(); -// } - // DIOCSTART ioctl_none!(pf_start, b'D', 1); @@ -774,7 +753,7 @@ mod tests { } #[test] - fn check_pf_addr_wrap() { + fn check_addr_wrap() { let ipnetv4 = IpNetwork::V4(Ipv4Network::new(Ipv4Addr::LOCALHOST, 8).unwrap()); let addr_wrap = AddrWrap::new(ipnetv4); From 341c5732939b73e51ce18cdcae5ae1faa5d3cab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Thu, 22 May 2025 12:06:44 +0200 Subject: [PATCH 06/18] Work in progress --- src/enterprise/firewall/api.rs | 4 + src/enterprise/firewall/mod.rs | 43 +++-- src/enterprise/firewall/nftables/netfilter.rs | 9 +- src/enterprise/firewall/packetfilter/calls.rs | 9 +- src/enterprise/firewall/packetfilter/mod.rs | 28 ++-- src/enterprise/firewall/packetfilter/rule.rs | 147 ++++++++++++++---- src/gateway.rs | 12 +- 7 files changed, 183 insertions(+), 69 deletions(-) diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index 12bc3f34..6327837c 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -13,6 +13,8 @@ pub struct FirewallApi { pub(crate) ifname: String, #[cfg(any(target_os = "freebsd", target_os = "macos"))] pub(crate) file: File, + #[cfg(any(target_os = "freebsd", target_os = "macos"))] + pub(crate) default_policy: Policy, #[cfg(target_os = "linux")] pub(crate) batch: Option, } @@ -23,6 +25,8 @@ impl FirewallApi { ifname: ifname.into(), #[cfg(any(target_os = "freebsd", target_os = "macos"))] file: OpenOptions::new().read(true).write(true).open(DEV_PF)?, + #[cfg(any(target_os = "freebsd", target_os = "macos"))] + default_policy: Policy::Deny, #[cfg(target_os = "linux")] batch: None, }) diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index d50d0541..31da915b 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -7,7 +7,7 @@ mod nftables; #[cfg(any(target_os = "freebsd", target_os = "macos"))] mod packetfilter; -use std::{net::IpAddr, str::FromStr}; +use std::{fmt, net::IpAddr, str::FromStr}; use ipnetwork::IpNetwork; use iprange::{IpAddrRange, IpAddrRangeError}; @@ -102,6 +102,16 @@ impl Port { } } +impl fmt::Display for Port { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Port::Any => Ok(()), // nothing here + Port::Single(port) => write!(f, "port = {port}"), + Port::Range(from, to) => write!(f, "port = {{{from}..{to}}}"), + } + } +} + /// As defined in `netinet/in.h`. #[derive(Debug, Copy, Clone, PartialEq)] #[repr(u8)] @@ -115,7 +125,7 @@ pub(crate) enum Protocol { impl Protocol { #[must_use] - pub(crate) fn supports_ports(&self) -> bool { + pub(crate) fn supports_ports(self) -> bool { matches!(self, Protocol::Tcp | Protocol::Udp) } } @@ -128,6 +138,7 @@ impl Protocol { proto::enterprise::firewall::Protocol::Tcp => Ok(Self::Tcp), proto::enterprise::firewall::Protocol::Udp => Ok(Self::Udp), proto::enterprise::firewall::Protocol::Icmp => Ok(Self::Icmp), + // TODO: IcmpV6 proto::enterprise::firewall::Protocol::Invalid => { Err(FirewallError::UnsupportedProtocol(proto as u8)) } @@ -135,6 +146,19 @@ impl Protocol { } } +impl fmt::Display for Protocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let protocol = match self { + Self::Any => "any", + Self::Icmp => "icmp", + Self::Tcp => "tcp", + Self::Udp => "udp", + Self::IcmpV6 => "icmp6", + }; + write!(f, "{protocol}") + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] pub(crate) enum Policy { #[default] @@ -162,14 +186,6 @@ impl Policy { } } -// struct AddressIter<'a>(&'a [Address]); - -// impl<'a> Iterator for AddressIter<'a> { -// type Item = IpNetwork; - -// fn next(&mut self) -> Option {} -// } - #[derive(Debug, Clone, PartialEq)] pub(crate) struct FirewallRule { pub comment: Option, @@ -183,12 +199,6 @@ pub(crate) struct FirewallRule { pub ipv4: bool, // FIXME: is that really needed? } -// impl FirewallRule { -// pub fn iter_source_addrs(&self) -> AddressIter { -// AddressIter(self.destination_addrs.as_slice()) -// } -// } - #[derive(Debug, Clone, PartialEq)] pub(crate) struct FirewallConfig { pub rules: Vec, @@ -273,6 +283,7 @@ pub enum FirewallError { IpAddrRange(#[from] IpAddrRangeError), #[error("Io error: {0}")] Io(#[from] std::io::Error), + #[cfg(any(target_os = "freebsd", target_os = "macos"))] #[error("Errno:{0}")] Errno(#[from] nix::errno::Errno), #[error("Type conversion error: {0}")] diff --git a/src/enterprise/firewall/nftables/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs index e65ded55..a8b66a72 100644 --- a/src/enterprise/firewall/nftables/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -3,7 +3,6 @@ use std::str::FromStr; use std::{ ffi::{CStr, CString}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, - ops::Range, }; use ipnetwork::IpNetwork; @@ -109,11 +108,11 @@ fn add_address_to_set(set: *mut nftnl_sys::nftnl_set, ip: &Address) -> Result<() } } Address::Range(addr_range) => match addr_range { - IpAddrRange::V4(Range { start, end }) => { - add_to_set(set, start, Some(end))?; + IpAddrRange::V4(ipv4_range) => { + add_to_set(set, ipv4_range.start(), Some(ipv4_range.end()))?; } - IpAddrRange::V6(Range { start, end }) => { - add_to_set(set, start, Some(end))?; + IpAddrRange::V6(ipv6_range) => { + add_to_set(set, ipv6_range.start(), Some(ipv6_range.end()))?; } }, } diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index e2dab019..fee66efd 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -54,9 +54,7 @@ impl From for AddrMask { #[repr(C)] struct AddrWrap { v: AddrMask, - // pub v: pf_addr_wrap_v, // unused in this crate - // pub p: pf_addr_wrap_p, p: u64, r#type: AddrType, iflags: c_uchar, @@ -516,7 +514,6 @@ pub(crate) enum RuleFlag { // ... } -// 1024 bytes pub(crate) const MAXPATHLEN: usize = libc::PATH_MAX as usize; /// Equivalent to `struct pfioc_rule`. @@ -639,12 +636,18 @@ impl IocTrans { } // DIOCSTART +// Start the packet filter. ioctl_none!(pf_start, b'D', 1); // DIOCSTOP +// Stop the packet filter. ioctl_none!(pf_stop, b'D', 2); // DIOCADDRULE +// Add rule at the end of the inactive ruleset. This call requires a ticket obtained through +// a preceding DIOCXBEGIN call and a pool_ticket obtained through a DIOCBEGINADDRS call. +// DIOCADDADDR must also be called if any pool addresses are required. The optional anchor name +// indicates the anchor in which to append the rule. `nr` and `action` are ignored. ioctl_readwrite!(pf_add_rule, b'D', 4, IocRule); // DIOCGETRULES diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index b91d090e..17208700 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -34,36 +34,39 @@ use super::{ FirewallError, FirewallRule, Policy, }; -const ANCHOR_PREFIX: &str = "defguard."; +const ANCHOR_PREFIX: &str = "defguard/"; impl FirewallApi { + /// Construct anchor name based on prefix and network interface name. fn anchor(&self) -> String { ANCHOR_PREFIX.to_owned() + &self.ifname } + /// Return raw file descriptor to Packet Filter device. fn fd(&self) -> RawFd { self.file.as_raw_fd() } - /// Add fireall `rules`. + /// Add a single firewall `rule`. fn add_rule( &mut self, - rule: FirewallRule, + rule: &mut FirewallRule, ticket: u32, pool_ticket: u32, anchor: &str, ) -> Result<(), FirewallError> { warn!("add_rule {rule:?}"); let rules = PacketFilterRule::from_firewall_rule(&self.ifname, rule); - warn!("--> rules {rules:?}"); for rule in rules { + warn!("--> {rule}"); let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); ioc.action = Change::None; ioc.ticket = ticket; ioc.pool_ticket = pool_ticket; - unsafe { - pf_add_rule(self.fd(), &mut ioc)?; + if let Err(err) = unsafe { pf_add_rule(self.fd(), &mut ioc) } { + error!("Packet filter rule {rule} can't be added."); + return Err(err.into()); } } @@ -86,10 +89,11 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - /// Add fireall `rules`. + /// Add firewall `rules`. fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { let anchor = &self.anchor(); // Begin transaction. + debug!("Begin pf transaction."); let element = IocTransElement::new(RuleSet::Filter, anchor); let mut elements = [element]; let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); @@ -101,8 +105,11 @@ impl FirewallManagementApi for FirewallApi { let ticket = elements[0].ticket; let pool_ticket = get_pool_ticket(self.fd(), anchor); - for rule in rules { - if let Err(err) = self.add_rule(rule, ticket, pool_ticket, anchor) { + for mut rule in rules { + if let Err(err) = self.add_rule(&mut rule, ticket, pool_ticket, anchor) { + error!("Firewall rule {} can't be added.", &rule.id); + debug!("Rollback pf transaction."); + // Rule cannot be added, so rollback. unsafe { pf_rollback(self.fd(), &mut ioc_trans)?; return Err(FirewallError::TransactionFailed(err.to_string())); @@ -110,6 +117,8 @@ impl FirewallManagementApi for FirewallApi { } } + // Commit transaction. + debug!("Commit pf transaction."); unsafe { pf_commit(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); } @@ -119,6 +128,7 @@ impl FirewallManagementApi for FirewallApi { /// Set default firewall policy. fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { + self.default_policy = policy; Ok(()) } diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index d7bdac7c..adf4f1b5 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -1,3 +1,5 @@ +use std::fmt; + use ipnetwork::IpNetwork; use libc::{AF_INET, AF_INET6, AF_UNSPEC}; @@ -35,9 +37,28 @@ pub enum Action { // PF_NONAT64 = 14, } +impl fmt::Display for Action { + /// Display `Action` as pf.conf keyword. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let action = match self { + Self::Pass => "pass", + Self::Drop => "block drop", + Self::Scrub => "scrub", + Self::NoScrub => "block scrub", + Self::Nat => "nat", + Self::NoNat => "block nat", + Self::BiNat => "binat", + Self::NoBiNat => "block binat", + Self::Redirect => "rdr", + Self::NoRedirect => "block rdr", + }; + write!(f, "{action}") + } +} + #[derive(Clone, Copy, Debug)] #[repr(u8)] -pub(crate) enum AddressFamily { +pub(super) enum AddressFamily { Unspec = AF_UNSPEC as u8, Inet = AF_INET as u8, Inet6 = AF_INET6 as u8, @@ -55,6 +76,18 @@ pub enum Direction { Out, } +impl fmt::Display for Direction { + /// Display `Direction` as pf.conf keyword. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let direction = match self { + Self::InOut => "", + Self::In => "in", + Self::Out => "out", + }; + write!(f, "{direction}") + } +} + const PF_LOG: u8 = 0x01; const PF_LOG_ALL: u8 = 0x02; const PF_LOG_SOCKET_LOOKUP: u8 = 0x04; @@ -118,31 +151,31 @@ const TH_ECE: u8 = 0x40; const TH_CWR: u8 = 0x80; #[derive(Debug)] -pub struct PacketFilterRule { +pub(super) struct PacketFilterRule { /// Source address; `Option::None` means "any". - pub(crate) from: Option, + pub(super) from: Option, /// Source port; 0 means "any". - pub(crate) from_port: Port, + pub(super) from_port: Port, /// Destination address; `Option::None` means "any". - pub(crate) to: Option, + pub(super) to: Option, /// Destination port; 0 means "any". - pub(crate) to_port: Port, - pub(crate) action: Action, - pub(crate) direction: Direction, - pub(crate) quick: bool, + pub(super) to_port: Port, + pub(super) action: Action, + pub(super) direction: Direction, + pub(super) quick: bool, /// See `LogFlags`. - pub(crate) log: u8, - pub(crate) keep_state: State, - pub(crate) interface: Option, - pub(crate) proto: Protocol, - pub(crate) tcp_flags: u8, - pub(crate) tcp_flags_set: u8, - pub(crate) label: Option, + pub(super) log: u8, + pub(super) keep_state: State, + pub(super) interface: Option, + pub(super) proto: Protocol, + pub(super) tcp_flags: u8, + pub(super) tcp_flags_set: u8, + pub(super) label: Option, } impl PacketFilterRule { /// Determine address family based on `to` field. - pub(crate) fn address_family(&self) -> AddressFamily { + pub(super) fn address_family(&self) -> AddressFamily { match self.to { None => AddressFamily::Unspec, Some(IpNetwork::V4(_)) => AddressFamily::Inet, @@ -151,7 +184,7 @@ impl PacketFilterRule { } /// Expand `FirewallRule` into a set of `PacketFilterRule`s. - pub(crate) fn from_firewall_rule(ifname: &str, mut fr: FirewallRule) -> Vec { + pub(super) fn from_firewall_rule(ifname: &str, fr: &mut FirewallRule) -> Vec { let mut rules = Vec::new(); let action = match fr.verdict { Policy::Allow => Action::Pass, @@ -162,11 +195,11 @@ impl PacketFilterRule { if fr.source_addrs.is_empty() { from_addrs.push(None); } else { - for src in fr.source_addrs { + for src in &fr.source_addrs { match src { - Address::Network(net) => from_addrs.push(Some(net)), + Address::Network(net) => from_addrs.push(Some(*net)), Address::Range(range) => { - for addr in range { + for addr in range.clone() { from_addrs.push(Some(IpNetwork::from(addr))); } } @@ -178,11 +211,11 @@ impl PacketFilterRule { if fr.destination_addrs.is_empty() { to_addrs.push(None); } else { - for src in fr.destination_addrs { + for src in &fr.destination_addrs { match src { - Address::Network(net) => to_addrs.push(Some(net)), + Address::Network(net) => to_addrs.push(Some(*net)), Address::Range(range) => { - for addr in range { + for addr in range.clone() { to_addrs.push(Some(IpNetwork::from(addr))); } } @@ -230,6 +263,39 @@ impl PacketFilterRule { } } +impl fmt::Display for PacketFilterRule { + // Display `PacketFilterRule` in similar format to rules in pf.conf. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.action, self.direction)?; + // TODO: log + if self.quick { + write!(f, " quick")?; + } + if let Some(interface) = &self.interface { + write!(f, " on {interface}")?; + } + write!(f, " from")?; + if let Some(from) = self.from { + write!(f, " {from}")?; + } else { + write!(f, " any")?; + } + write!(f, " {} to", self.from_port)?; + if let Some(to) = self.to { + write!(f, " {to}")?; + } else { + write!(f, " any")?; + } + write!(f, " {}", self.to_port)?; + // TODO: tcp_flags/tcp_flags_set, keep_state + if let Some(label) = &self.label { + write!(f, " label \"{label}\"")?; + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use std::net::{IpAddr, Ipv4Addr}; @@ -239,7 +305,7 @@ mod tests { #[test] fn unroll_firewall_rule() { // Empty rule - let fr = FirewallRule { + let mut fr = FirewallRule { comment: None, destination_addrs: Vec::new(), destination_ports: Vec::new(), @@ -250,14 +316,15 @@ mod tests { ipv4: true, }; - let rules = PacketFilterRule::from_firewall_rule("lo0", fr); + let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); assert_eq!(1, rules.len()); + assert_eq!(rules[0].to_string(), "pass on lo0 from any to any "); // One address, one port. let addr1 = Address::Network( IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), 24).unwrap(), ); - let fr = FirewallRule { + let mut fr = FirewallRule { comment: None, destination_addrs: vec![addr1], destination_ports: vec![Port::Single(1138)], @@ -268,8 +335,12 @@ mod tests { ipv4: true, }; - let rules = PacketFilterRule::from_firewall_rule("lo0", fr); + let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); assert_eq!(1, rules.len()); + assert_eq!( + rules[0].to_string(), + "pass on lo0 from any to 192.168.1.10/24 port = 1138" + ); // Two addresses, two ports. let addr1 = Address::Network( @@ -278,7 +349,7 @@ mod tests { let addr2 = Address::Network( IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), 24).unwrap(), ); - let fr = FirewallRule { + let mut fr = FirewallRule { comment: None, destination_addrs: vec![addr1, addr2], destination_ports: vec![Port::Single(1138), Port::Single(42)], @@ -289,7 +360,23 @@ mod tests { ipv4: true, }; - let rules = PacketFilterRule::from_firewall_rule("lo0", fr); + let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); assert_eq!(4, rules.len()); + assert_eq!( + rules[0].to_string(), + "pass on lo0 from any to 192.168.1.10/24 port = 1138" + ); + assert_eq!( + rules[1].to_string(), + "pass on lo0 from any to 192.168.1.10/24 port = 42" + ); + assert_eq!( + rules[2].to_string(), + "pass on lo0 from any to 192.168.1.20/24 port = 1138" + ); + assert_eq!( + rules[3].to_string(), + "pass on lo0 from any to 192.168.1.20/24 port = 42" + ); } } diff --git a/src/gateway.rs b/src/gateway.rs index 3117fecf..3f64ec8d 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -512,7 +512,7 @@ impl Gateway { } Some(update::Update::FirewallConfig(config)) => { debug!("Applying received firewall configuration: {config:?}"); - let config_str = format!("{:?}", config); + let config_str = format!("{config:?}"); match FirewallConfig::from_proto(config) { Ok(new_firewall_config) => { debug!( @@ -537,7 +537,7 @@ impl Gateway { } } } - Some(update::Update::DisableFirewall(_)) => { + Some(update::Update::DisableFirewall(())) => { debug!("Disabling firewall configuration"); if let Err(err) = self.process_firewall_changes(None) { error!("Failed to disable firewall configuration: {err}"); @@ -963,12 +963,12 @@ mod tests { let config5 = FirewallConfig { rules: vec![FirewallRule { comment: None, - destination_addrs: vec![], - destination_ports: vec![], + destination_addrs: Vec::new(), + destination_ports: Vec::new(), id: 0, verdict: Policy::Allow, - protocols: vec![], - source_addrs: vec![], + protocols: Vec::new(), + source_addrs: Vec::new(), ipv4: false, }], default_policy: Policy::Allow, From 224103b1cc9456c90e6f3fd662d2a1bd0640b6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 23 May 2025 11:52:19 +0200 Subject: [PATCH 07/18] PF: add default policy; enable quick rules; fix build on FreeBSD; handle rtableid --- Cargo.lock | 16 +++-- src/enterprise/firewall/nftables/netfilter.rs | 2 +- src/enterprise/firewall/packetfilter/calls.rs | 14 +++-- src/enterprise/firewall/packetfilter/mod.rs | 34 ++++++++++- src/enterprise/firewall/packetfilter/rule.rs | 59 ++++++++++++++----- .../firewall/packetfilter/ticket.rs | 8 +-- 6 files changed, 102 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19464de7..41088095 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,12 +67,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -1343,6 +1343,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl-probe" version = "0.1.6" @@ -1693,9 +1699,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" diff --git a/src/enterprise/firewall/nftables/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs index a8b66a72..e9b7d4a6 100644 --- a/src/enterprise/firewall/nftables/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -423,7 +423,7 @@ impl FirewallRule for FilterRule<'_> { )) })?; rule.set_comment(comment); - debug!("Added comment to nftables expression: {:?}", comment_string); + debug!("Added comment to nftables expression: {comment_string:?}"); } else { debug!("No comment provided for nftables expression"); } diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index fee66efd..63003cad 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -6,7 +6,7 @@ use std::{ }; use ipnetwork::IpNetwork; -use libc::{pid_t, sa_family_t, uid_t, IFNAMSIZ}; +use libc::{pid_t, uid_t, IFNAMSIZ}; use nix::{ioctl_none, ioctl_readwrite}; use super::rule::{Action, AddressFamily, Direction, PacketFilterRule, RuleSet, State}; @@ -195,7 +195,7 @@ pub(super) struct Pool { port_op: PortOp, opts: c_uchar, #[cfg(target_os = "macos")] - af: sa_family_t, + af: AddressFamily, // sa_family_t, } #[derive(Debug)] @@ -350,7 +350,7 @@ pub(super) struct Rule { os_fingerprint: c_uint, - rtableid: c_uint, + rtableid: c_int, #[cfg(target_os = "freebsd")] timeout: [c_uint; 20], #[cfg(target_os = "macos")] @@ -424,7 +424,7 @@ pub(super) struct Rule { set_prio: [c_uchar; 2], #[cfg(target_os = "freebsd")] - divert: (pf_addr, u16), + divert: (Addr, u16), #[cfg(target_os = "freebsd")] u_states_cur: u64, @@ -465,6 +465,10 @@ impl Rule { let len = label.len().min(PF_RULE_LABEL_SIZE - 1); (*self_ptr).label[..len].copy_from_slice(&label.as_bytes()[..len]); } + + // Don't use routing tables. + (*self_ptr).rtableid = -1; + (*self_ptr).action = pf_rule.action; (*self_ptr).direction = pf_rule.direction; (*self_ptr).log = pf_rule.log; @@ -750,7 +754,7 @@ mod tests { assert_eq!(align_of::(), 8); #[cfg(target_os = "freebsd")] - assert_eq!(size_of::(), 3104); + assert_eq!(size_of::(), 3040); #[cfg(target_os = "macos")] assert_eq!(size_of::(), 3104); } diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index 17208700..a6ddc9eb 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -47,6 +47,26 @@ impl FirewallApi { self.file.as_raw_fd() } + fn add_rule_policy( + &mut self, + ticket: u32, + pool_ticket: u32, + anchor: &str, + ) -> Result<(), FirewallError> { + let rule = PacketFilterRule::for_policy(self.default_policy, &self.ifname); + warn!("==> {rule}"); + let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); + ioc.action = Change::None; + ioc.ticket = ticket; + ioc.pool_ticket = pool_ticket; + if let Err(err) = unsafe { pf_add_rule(self.fd(), &mut ioc) } { + error!("Packet filter rule {rule} can't be added."); + return Err(err.into()); + } + + Ok(()) + } + /// Add a single firewall `rule`. fn add_rule( &mut self, @@ -78,9 +98,10 @@ impl FirewallApi { impl FirewallManagementApi for FirewallApi { fn setup( &mut self, - _default_policy: Policy, + default_policy: Policy, _priority: Option, ) -> Result<(), FirewallError> { + self.default_policy = default_policy; Ok(()) } @@ -105,6 +126,17 @@ impl FirewallManagementApi for FirewallApi { let ticket = elements[0].ticket; let pool_ticket = get_pool_ticket(self.fd(), anchor); + // Create first rule from the default policy. + if let Err(err) = self.add_rule_policy(ticket, pool_ticket, anchor) { + error!("Default policy rule can't be added."); + debug!("Rollback pf transaction."); + // Rule cannot be added, so rollback. + unsafe { + pf_rollback(self.fd(), &mut ioc_trans)?; + return Err(FirewallError::TransactionFailed(err.to_string())); + } + } + for mut rule in rules { if let Err(err) = self.add_rule(&mut rule, ticket, pool_ticket, anchor) { error!("Firewall rule {} can't be added.", &rule.id); diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index adf4f1b5..d88578ab 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -1,7 +1,7 @@ use std::fmt; use ipnetwork::IpNetwork; -use libc::{AF_INET, AF_INET6, AF_UNSPEC}; +use libc::{dirfd, AF_INET, AF_INET6, AF_UNSPEC}; use super::{FirewallRule, Port}; use crate::enterprise::firewall::{Address, Policy, Protocol}; @@ -90,11 +90,11 @@ impl fmt::Display for Direction { const PF_LOG: u8 = 0x01; const PF_LOG_ALL: u8 = 0x02; -const PF_LOG_SOCKET_LOOKUP: u8 = 0x04; -#[cfg(target_os = "freebsd")] -const PF_LOG_FORCE: u8 = 0x08; -#[cfg(target_os = "freebsd")] -const PF_LOG_MATCHES: u8 = 0x10; +// const PF_LOG_SOCKET_LOOKUP: u8 = 0x04; +// #[cfg(target_os = "freebsd")] +// const PF_LOG_FORCE: u8 = 0x08; +// #[cfg(target_os = "freebsd")] +// const PF_LOG_MATCHES: u8 = 0x10; /// Equivalent to `PF_RULESET_...`. #[derive(Clone, Copy, Debug)] @@ -163,7 +163,7 @@ pub(super) struct PacketFilterRule { pub(super) action: Action, pub(super) direction: Direction, pub(super) quick: bool, - /// See `LogFlags`. + /// See `PF_LOG`. pub(super) log: u8, pub(super) keep_state: State, pub(super) interface: Option, @@ -174,6 +174,31 @@ pub(super) struct PacketFilterRule { } impl PacketFilterRule { + /// Default rule for policy. + #[must_use] + pub(super) fn for_policy(policy: Policy, ifname: &str) -> Self { + let action = match policy { + Policy::Allow => Action::Pass, + Policy::Deny => Action::Drop, + }; + Self { + from: None, + from_port: Port::Any, + to: None, + to_port: Port::Any, + action, + direction: Direction::In, + quick: false, + log: 0, + keep_state: State::None, + interface: Some(ifname.to_owned()), + proto: Protocol::Any, + tcp_flags: 0, + tcp_flags_set: 0, + label: None, + } + } + /// Determine address family based on `to` field. pub(super) fn address_family(&self) -> AddressFamily { match self.to { @@ -241,8 +266,9 @@ impl PacketFilterRule { to: *to, to_port: *to_port, action, - direction: Direction::InOut, - quick: false, + direction: Direction::In, + // Enable quick to match NFTables behaviour. + quick: true, // Disable logging. log: 0, keep_state: State::Normal, @@ -318,7 +344,10 @@ mod tests { let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); assert_eq!(1, rules.len()); - assert_eq!(rules[0].to_string(), "pass on lo0 from any to any "); + assert_eq!( + rules[0].to_string(), + "pass in quick on lo0 from any to any " + ); // One address, one port. let addr1 = Address::Network( @@ -339,7 +368,7 @@ mod tests { assert_eq!(1, rules.len()); assert_eq!( rules[0].to_string(), - "pass on lo0 from any to 192.168.1.10/24 port = 1138" + "pass in quick on lo0 from any to 192.168.1.10/24 port = 1138" ); // Two addresses, two ports. @@ -364,19 +393,19 @@ mod tests { assert_eq!(4, rules.len()); assert_eq!( rules[0].to_string(), - "pass on lo0 from any to 192.168.1.10/24 port = 1138" + "pass in quick on lo0 from any to 192.168.1.10/24 port = 1138" ); assert_eq!( rules[1].to_string(), - "pass on lo0 from any to 192.168.1.10/24 port = 42" + "pass in quick on lo0 from any to 192.168.1.10/24 port = 42" ); assert_eq!( rules[2].to_string(), - "pass on lo0 from any to 192.168.1.20/24 port = 1138" + "pass in quick on lo0 from any to 192.168.1.20/24 port = 1138" ); assert_eq!( rules[3].to_string(), - "pass on lo0 from any to 192.168.1.20/24 port = 42" + "pass in quick on lo0 from any to 192.168.1.20/24 port = 42" ); } } diff --git a/src/enterprise/firewall/packetfilter/ticket.rs b/src/enterprise/firewall/packetfilter/ticket.rs index 70111cec..2aa55186 100644 --- a/src/enterprise/firewall/packetfilter/ticket.rs +++ b/src/enterprise/firewall/packetfilter/ticket.rs @@ -7,7 +7,7 @@ use super::{ rule::Action, }; -pub fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> u32 { +pub(super) fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> u32 { let mut pfioc_rule = IocRule::new(anchor); pfioc_rule.action = Change::GetTicket; @@ -27,7 +27,7 @@ pub fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> u32 { pfioc_rule.ticket } -pub fn get_pool_ticket(fd: RawFd, anchor: &str) -> u32 { +pub(super) fn get_pool_ticket(fd: RawFd, anchor: &str) -> u32 { let mut ioc = IocPoolAddr::new(anchor); unsafe { @@ -37,8 +37,8 @@ pub fn get_pool_ticket(fd: RawFd, anchor: &str) -> u32 { ioc.ticket } -// Add pool address using the pool ticket previously obtained via `get_pool_ticket()` -pub fn add_pool_address(fd: RawFd, pool_addr: IpNetwork, pool_ticket: u32) { +// Add pool address using the pool ticket previously obtained via `get_pool_ticket()`. +pub(super) fn add_pool_address(fd: RawFd, pool_addr: IpNetwork, pool_ticket: u32) { let mut pfioc_pooladdr = unsafe { std::mem::zeroed::() }; pfioc_pooladdr.ticket = pool_ticket; pfioc_pooladdr.addr = PoolAddr::new(pool_addr, ""); // XXX: ifname From 5eb8037859916713263cf6894fe4e5733295a6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 26 May 2025 16:24:03 +0200 Subject: [PATCH 08/18] Work in progress --- Cargo.lock | 55 ++++----- Cargo.toml | 9 +- src/enterprise/firewall/api.rs | 12 +- src/enterprise/firewall/mod.rs | 6 +- src/enterprise/firewall/packetfilter/calls.rs | 116 +++++++++++------- src/enterprise/firewall/packetfilter/mod.rs | 6 +- src/enterprise/firewall/packetfilter/rule.rs | 51 +++++--- .../firewall/packetfilter/ticket.rs | 36 +++--- src/gateway.rs | 4 +- src/main.rs | 6 +- 10 files changed, 167 insertions(+), 134 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41088095..67d70b01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,9 +281,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.23" +version = "1.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ "jobserver", "libc", @@ -350,9 +350,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "defguard-gateway" -version = "1.3.0" +version = "1.3.1" dependencies = [ "axum 0.8.4", "base64", @@ -458,7 +458,7 @@ dependencies = [ "log", "mnl", "nftnl", - "nix 0.30.1", + "nix", "prost", "prost-build", "serde", @@ -475,8 +475,7 @@ dependencies = [ [[package]] name = "defguard_wireguard_rs" -version = "0.7.2" -source = "git+https://github.com/DefGuard/wireguard-rs.git?rev=v0.7.2#6538ef726645604e7d466a4f7c5365a1b4629f86" +version = "0.7.3" dependencies = [ "base64", "libc", @@ -487,7 +486,7 @@ dependencies = [ "netlink-packet-utils", "netlink-packet-wireguard", "netlink-sys", - "nix 0.29.0", + "nix", "serde", "thiserror 2.0.12", "x25519-dalek", @@ -1157,13 +1156,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1288,19 +1287,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.30.1" @@ -1311,6 +1297,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -1816,6 +1803,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.9" @@ -1833,9 +1829,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1997,15 +1993,16 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index e94c4eab..20e10eb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "defguard-gateway" -version = "1.3.0" +version = "1.3.1" edition = "2021" [dependencies] axum = { version = "0.8", features = ["macros"] } base64 = "0.22" clap = { version = "4.5", features = ["derive", "env"] } -defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.7.2" } +# defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.7.3" } +defguard_wireguard_rs = { path = "../wireguard-rs" } env_logger = "0.11" gethostname = "1.0" ipnetwork = "0.21" @@ -18,7 +19,7 @@ serde = { version = "1.0", features = ["derive"] } syslog = "7.0" thiserror = "2.0" tonic = { version = "0.12", features = ["gzip", "tls", "tls-native-roots"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } tokio-stream = { version = "0.1", features = [] } toml = { version = "0.8", default-features = false, features = ["parse"] } @@ -26,7 +27,7 @@ toml = { version = "0.8", default-features = false, features = ["parse"] } nftnl = { git = "https://github.com/DefGuard/nftnl-rs.git", rev = "1a1147271f43b9d7182a114bb056a5224c35d38f" } mnl = "0.2" -[target.'cfg(any(target_os = "freebsd", target_os = "macos"))'.dependencies] +[target.'cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))'.dependencies] nix = { version = "0.30", default-features = false, features = ["ioctl"] } [dev-dependencies] diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index 6327837c..e301de5b 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -1,4 +1,4 @@ -#[cfg(any(target_os = "freebsd", target_os = "macos"))] +#[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] use std::fs::{File, OpenOptions}; #[cfg(target_os = "linux")] @@ -6,14 +6,14 @@ use nftnl::Batch; use super::{FirewallError, FirewallRule, Policy}; -#[cfg(any(target_os = "freebsd", target_os = "macos"))] +#[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] const DEV_PF: &str = "/dev/pf"; pub struct FirewallApi { pub(crate) ifname: String, - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] pub(crate) file: File, - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] pub(crate) default_policy: Policy, #[cfg(target_os = "linux")] pub(crate) batch: Option, @@ -23,9 +23,9 @@ impl FirewallApi { pub fn new>(ifname: S) -> Result { Ok(Self { ifname: ifname.into(), - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] file: OpenOptions::new().read(true).write(true).open(DEV_PF)?, - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] default_policy: Policy::Deny, #[cfg(target_os = "linux")] batch: None, diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index 31da915b..4c8b1a7e 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -4,7 +4,7 @@ mod dummy; mod iprange; #[cfg(all(not(test), target_os = "linux"))] mod nftables; -#[cfg(any(target_os = "freebsd", target_os = "macos"))] +#[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] mod packetfilter; use std::{fmt, net::IpAddr, str::FromStr}; @@ -128,9 +128,7 @@ impl Protocol { pub(crate) fn supports_ports(self) -> bool { matches!(self, Protocol::Tcp | Protocol::Udp) } -} -impl Protocol { pub(crate) const fn from_proto( proto: proto::enterprise::firewall::Protocol, ) -> Result { @@ -283,7 +281,7 @@ pub enum FirewallError { IpAddrRange(#[from] IpAddrRangeError), #[error("Io error: {0}")] Io(#[from] std::io::Error), - #[cfg(any(target_os = "freebsd", target_os = "macos"))] + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] #[error("Errno:{0}")] Errno(#[from] nix::errno::Errno), #[error("Type conversion error: {0}")] diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index 63003cad..d3b5a80b 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -99,11 +99,14 @@ pub(super) struct RuleAddr { addr: AddrWrap, // macOS: here `union pf_rule_xport` is flattened to its first variant: `struct pf_port_range`. port: [c_ushort; 2], + #[cfg(any(target_os = "freebsd", target_os = "macos"))] op: PortOp, #[cfg(target_os = "macos")] _padding: [c_uchar; 3], - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "netbsd"))] neg: c_uchar, + #[cfg(target_os = "netbsd")] + op: PortOp, } impl RuleAddr { @@ -136,32 +139,26 @@ impl RuleAddr { op, #[cfg(target_os = "macos")] _padding: [0; 3], - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "netbsd"))] neg: 0, } } } -/// TAILQ_ENTRY -#[derive(Debug)] -#[repr(C)] -struct pf_rule_list { - tqe_next: *mut Rule, - tqe_prev: *mut *mut Rule, -} - +// TAILQ_ENTRY #[derive(Debug)] #[repr(C)] -struct pf_pooladdr_list { - tqe_next: *mut PoolAddr, - tqe_prev: *mut *mut PoolAddr, +struct List { + tqe_next: *mut T, + tqe_prev: *mut *mut T, } // Equivalent to `struct pf_pooladdr`. +#[derive(Debug)] #[repr(C)] pub struct PoolAddr { addr: AddrWrap, - entries: pf_pooladdr_list, + entries: List, ifname: [u8; IFNAMSIZ], kif: usize, // *mut c_void, } @@ -174,26 +171,41 @@ impl PoolAddr { ifname[..len].copy_from_slice(&if_name.as_bytes()[..len]); Self { addr: AddrWrap::new(ip_network), - entries: unsafe { std::mem::zeroed::() }, + entries: unsafe { std::mem::zeroed::>() }, ifname, kif: 0, } } } +#[derive(Debug)] +#[repr(u8)] +pub(super) enum PoolOpts { + /// PF_POOL_NONE = 0 + None, + /// PF_POOL_BITMASK = 1 + BitMask, + /// PF_POOL_RANDOM = 2 + Random, + /// PF_POOL_SRCHASH = 3 + SrcHash, + /// PF_POOL_ROUNDROBIN = 4 + RoundRobin, +} + /// Equivalent to `struct pf_pool`. #[derive(Debug)] #[repr(C)] pub(super) struct Pool { - list: pf_pooladdr_list, + list: List, cur: *mut c_void, key: PoolHashKey, counter: Addr, tblidx: c_int, - proxy_port: [c_ushort; 2], - #[cfg(target_os = "macos")] + pub(super) proxy_port: [c_ushort; 2], + #[cfg(any(target_os = "macos", target_os = "netbsd"))] port_op: PortOp, - opts: c_uchar, + pub(super) opts: PoolOpts, #[cfg(target_os = "macos")] af: AddressFamily, // sa_family_t, } @@ -225,11 +237,12 @@ enum PortOp { impl Pool { #[must_use] - pub fn new(port: u16) -> Self { + pub fn new(from_port: u16, to_port: u16) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); unsafe { - (*self_ptr).proxy_port[0] = port; + (*self_ptr).proxy_port[0] = from_port; + (*self_ptr).proxy_port[1] = to_port; } unsafe { uninit.assume_init() } @@ -330,8 +343,8 @@ pub(super) struct Rule { match_tagname: [c_uchar; 64], overload_tblname: [c_uchar; 32], - entries: pf_rule_list, - rpool: Pool, + entries: List, + pub(super) rpool: Pool, evaluations: c_long, packets: [c_ulong; 2], @@ -351,14 +364,14 @@ pub(super) struct Rule { os_fingerprint: c_uint, rtableid: c_int, - #[cfg(target_os = "freebsd")] + #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] timeout: [c_uint; 20], #[cfg(target_os = "macos")] timeout: [c_uint; 26], - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "netbsd"))] states: c_uint, max_states: c_uint, - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "netbsd"))] src_nodes: c_uint, max_src_nodes: c_uint, max_src_states: c_uint, @@ -467,15 +480,27 @@ impl Rule { } // Don't use routing tables. - (*self_ptr).rtableid = -1; + #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] + { + (*self_ptr).rtableid = -1; + } + #[cfg(target_os = "macos")] + { + (*self_ptr).rtableid = 0; + } (*self_ptr).action = pf_rule.action; (*self_ptr).direction = pf_rule.direction; (*self_ptr).log = pf_rule.log; (*self_ptr).quick = pf_rule.quick; - (*self_ptr).keep_state = pf_rule.keep_state; - (*self_ptr).af = pf_rule.address_family(); + (*self_ptr).keep_state = pf_rule.state; + let af = pf_rule.address_family(); + (*self_ptr).af = af; + #[cfg(target_os = "macos")] + { + (*self_ptr).rpool.af = af; + } (*self_ptr).proto = pf_rule.proto as u8; (*self_ptr).flags = pf_rule.tcp_flags; (*self_ptr).flagset = pf_rule.tcp_flags_set; @@ -567,14 +592,14 @@ impl IocRule { #[repr(C)] pub(super) struct IocPoolAddr { action: Change, - pub ticket: c_uint, + pub(super) ticket: c_uint, nr: c_uint, r_num: c_uint, r_action: c_uchar, r_last: c_uchar, af: c_uchar, anchor: [c_uchar; MAXPATHLEN], - pub addr: PoolAddr, + addr: PoolAddr, } impl IocPoolAddr { @@ -598,7 +623,7 @@ impl IocPoolAddr { pub(super) struct IocTransElement { rs_num: RuleSet, anchor: [c_uchar; MAXPATHLEN], - pub ticket: c_uint, + pub(super) ticket: c_uint, } impl IocTransElement { @@ -607,7 +632,7 @@ impl IocTransElement { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); - // Set `RuleSet` and copy anchor name. + // Copy anchor name. let len = anchor.len().min(MAXPATHLEN - 1); unsafe { (*self_ptr).rs_num = ruleset; @@ -621,9 +646,9 @@ impl IocTransElement { /// Equivalent to `struct pfioc_trans`. #[repr(C)] pub(super) struct IocTrans { - /// number of elements + /// Number of elements. size: c_int, - /// size of each element in bytes + /// Size of each element in bytes. esize: c_int, array: *mut IocTransElement, } @@ -644,13 +669,13 @@ impl IocTrans { ioctl_none!(pf_start, b'D', 1); // DIOCSTOP -// Stop the packet filter. +// Stop the packet filter. ioctl_none!(pf_stop, b'D', 2); // DIOCADDRULE // Add rule at the end of the inactive ruleset. This call requires a ticket obtained through // a preceding DIOCXBEGIN call and a pool_ticket obtained through a DIOCBEGINADDRS call. -// DIOCADDADDR must also be called if any pool addresses are required. The optional anchor name +// DIOCADDADDR must also be called if any pool addresses are required. The optional anchor name // indicates the anchor in which to append the rule. `nr` and `action` are ignored. ioctl_readwrite!(pf_add_rule, b'D', 4, IocRule); @@ -686,19 +711,19 @@ ioctl_readwrite!(pf_delete_rule, b'D', 28, IocRule); // ioctl_readwrite!(pf_kill_states, b'D', 41, pfioc_state_kill); // DIOCBEGINADDRS -// #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))] +// Clear the buffer address pool and get a ticket for subsequent DIOCADDADDR, DIOCADDRULE, and +// DIOCCHANGERULE calls. ioctl_readwrite!(pf_begin_addrs, b'D', 51, IocPoolAddr); // DIOCADDADDR -// #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd"))] +// Add the pool address `addr` to the buffer address pool to be used in the following DIOCADDRULE +// or DIOCCHANGERULE call. All other members of the structure are ignored. ioctl_readwrite!(pf_add_addr, b'D', 52, IocPoolAddr); // DIOCGETRULESETS -// #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] // ioctl_readwrite!(pf_get_rulesets, b'D', 58, PFRuleset); // DIOCGETRULESET -// #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] // ioctl_readwrite!(pf_get_ruleset, b'D', 59, PFRuleset); // DIOCXBEGIN @@ -710,11 +735,6 @@ ioctl_readwrite!(pf_commit, b'D', 82, IocTrans); // DIOCXROLLBACK ioctl_readwrite!(pf_rollback, b'D', 83, IocTrans); -// DIOCXEND -// Required by OpenBSD to release the ticket obtained by the DIOCGETRULES command. -// #[cfg(target_os = "openbsd")] -// ioctl_readwrite!(pf_end_trans, b'D', 100, c_int); - #[cfg(test)] mod tests { use ipnetwork::{Ipv4Network, Ipv6Network}; @@ -751,12 +771,16 @@ mod tests { assert_eq!(size_of::(), 56); #[cfg(target_os = "macos")] assert_eq!(size_of::(), 64); + #[cfg(target_os = "netbsd")] + assert_eq!(size_of::(), 56); assert_eq!(align_of::(), 8); #[cfg(target_os = "freebsd")] assert_eq!(size_of::(), 3040); #[cfg(target_os = "macos")] assert_eq!(size_of::(), 3104); + #[cfg(target_os = "netbsd")] + assert_eq!(size_of::(), 2976); } #[test] diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index a6ddc9eb..9e4a3966 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -56,7 +56,6 @@ impl FirewallApi { let rule = PacketFilterRule::for_policy(self.default_policy, &self.ifname); warn!("==> {rule}"); let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); - ioc.action = Change::None; ioc.ticket = ticket; ioc.pool_ticket = pool_ticket; if let Err(err) = unsafe { pf_add_rule(self.fd(), &mut ioc) } { @@ -115,8 +114,7 @@ impl FirewallManagementApi for FirewallApi { let anchor = &self.anchor(); // Begin transaction. debug!("Begin pf transaction."); - let element = IocTransElement::new(RuleSet::Filter, anchor); - let mut elements = [element]; + let mut elements = [IocTransElement::new(RuleSet::Filter, anchor)]; let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); // This will create an anchor if it doesn't exist. unsafe { @@ -124,7 +122,7 @@ impl FirewallManagementApi for FirewallApi { } let ticket = elements[0].ticket; - let pool_ticket = get_pool_ticket(self.fd(), anchor); + let pool_ticket = get_pool_ticket(self.fd(), anchor)?; // Create first rule from the default policy. if let Err(err) = self.add_rule_policy(ticket, pool_ticket, anchor) { diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index d88578ab..311c74b0 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -1,4 +1,7 @@ -use std::fmt; +use std::{ + fmt, + net::{IpAddr, Ipv4Addr}, +}; use ipnetwork::IpNetwork; use libc::{dirfd, AF_INET, AF_INET6, AF_UNSPEC}; @@ -132,6 +135,19 @@ pub enum State { SynProxy = 3, } +impl fmt::Display for State { + /// Display `State` as in pf.conf. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let state = match self { + Self::None => "no state", + Self::Normal => "keep state", + Self::Modulate => "modulate state", + Self::SynProxy => "synproxy state", + }; + write!(f, "{state}") + } +} + /// TCP flags as defined in `netinet/tcp.h`. /// Final: Set on the last segment. const TH_FIN: u8 = 0x01; @@ -165,7 +181,7 @@ pub(super) struct PacketFilterRule { pub(super) quick: bool, /// See `PF_LOG`. pub(super) log: u8, - pub(super) keep_state: State, + pub(super) state: State, pub(super) interface: Option, pub(super) proto: Protocol, pub(super) tcp_flags: u8, @@ -190,7 +206,7 @@ impl PacketFilterRule { direction: Direction::In, quick: false, log: 0, - keep_state: State::None, + state: State::None, interface: Some(ifname.to_owned()), proto: Protocol::Any, tcp_flags: 0, @@ -199,10 +215,14 @@ impl PacketFilterRule { } } - /// Determine address family based on `to` field. + /// Determine address family. pub(super) fn address_family(&self) -> AddressFamily { match self.to { - None => AddressFamily::Unspec, + None => match self.from { + None => AddressFamily::Unspec, + Some(IpNetwork::V4(_)) => AddressFamily::Inet, + Some(IpNetwork::V6(_)) => AddressFamily::Inet6, + }, Some(IpNetwork::V4(_)) => AddressFamily::Inet, Some(IpNetwork::V6(_)) => AddressFamily::Inet6, } @@ -266,12 +286,13 @@ impl PacketFilterRule { to: *to, to_port: *to_port, action, - direction: Direction::In, + direction: Direction::Out, // Enable quick to match NFTables behaviour. quick: true, // Disable logging. log: 0, - keep_state: State::Normal, + // Keeping state is the default. + state: State::Normal, interface: Some(ifname.to_owned()), proto: *proto, // For stateful connections, the default is flags S/SA. @@ -312,8 +333,8 @@ impl fmt::Display for PacketFilterRule { } else { write!(f, " any")?; } - write!(f, " {}", self.to_port)?; - // TODO: tcp_flags/tcp_flags_set, keep_state + // TODO: tcp_flags/tcp_flags_set + write!(f, " {} {}", self.to_port, self.state)?; if let Some(label) = &self.label { write!(f, " label \"{label}\"")?; } @@ -346,7 +367,7 @@ mod tests { assert_eq!(1, rules.len()); assert_eq!( rules[0].to_string(), - "pass in quick on lo0 from any to any " + "pass out quick on lo0 from any to any keep state" ); // One address, one port. @@ -368,7 +389,7 @@ mod tests { assert_eq!(1, rules.len()); assert_eq!( rules[0].to_string(), - "pass in quick on lo0 from any to 192.168.1.10/24 port = 1138" + "pass out quick on lo0 from any to 192.168.1.10/24 port = 1138 keep state" ); // Two addresses, two ports. @@ -393,19 +414,19 @@ mod tests { assert_eq!(4, rules.len()); assert_eq!( rules[0].to_string(), - "pass in quick on lo0 from any to 192.168.1.10/24 port = 1138" + "pass out quick on lo0 from any to 192.168.1.10/24 port = 1138 keep state" ); assert_eq!( rules[1].to_string(), - "pass in quick on lo0 from any to 192.168.1.10/24 port = 42" + "pass out quick on lo0 from any to 192.168.1.10/24 port = 42 keep state" ); assert_eq!( rules[2].to_string(), - "pass in quick on lo0 from any to 192.168.1.20/24 port = 1138" + "pass out quick on lo0 from any to 192.168.1.20/24 port = 1138 keep state" ); assert_eq!( rules[3].to_string(), - "pass in quick on lo0 from any to 192.168.1.20/24 port = 42" + "pass out quick on lo0 from any to 192.168.1.20/24 port = 42 keep state" ); } } diff --git a/src/enterprise/firewall/packetfilter/ticket.rs b/src/enterprise/firewall/packetfilter/ticket.rs index 2aa55186..c02032b3 100644 --- a/src/enterprise/firewall/packetfilter/ticket.rs +++ b/src/enterprise/firewall/packetfilter/ticket.rs @@ -6,43 +6,37 @@ use super::{ calls::{pf_add_addr, pf_begin_addrs, pf_change_rule, Change, IocPoolAddr, IocRule, PoolAddr}, rule::Action, }; +use crate::enterprise::firewall::FirewallError; -pub(super) fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> u32 { +pub(super) fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> Result { let mut pfioc_rule = IocRule::new(anchor); pfioc_rule.action = Change::GetTicket; pfioc_rule.rule.action = kind; - // pfioc_rule.action is ignored on FreeBSD, NetBSD, and OpenBSD. --- REALLY? [adam] - // #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] - // { - // pfioc_rule.action = Change::GetTicket; - // pfioc_rule.rule.action = Change::GetTicket as u8; - // } - unsafe { - pf_change_rule(fd, &mut pfioc_rule).unwrap(); + pf_change_rule(fd, &mut pfioc_rule)?; } - pfioc_rule.ticket + Ok(pfioc_rule.ticket) } -pub(super) fn get_pool_ticket(fd: RawFd, anchor: &str) -> u32 { +pub(super) fn get_pool_ticket(fd: RawFd, anchor: &str) -> Result { let mut ioc = IocPoolAddr::new(anchor); unsafe { - pf_begin_addrs(fd, &mut ioc).unwrap(); + pf_begin_addrs(fd, &mut ioc)?; } - ioc.ticket + Ok(ioc.ticket) } // Add pool address using the pool ticket previously obtained via `get_pool_ticket()`. -pub(super) fn add_pool_address(fd: RawFd, pool_addr: IpNetwork, pool_ticket: u32) { - let mut pfioc_pooladdr = unsafe { std::mem::zeroed::() }; - pfioc_pooladdr.ticket = pool_ticket; - pfioc_pooladdr.addr = PoolAddr::new(pool_addr, ""); // XXX: ifname - unsafe { - pf_add_addr(fd, &mut pfioc_pooladdr).unwrap(); - } -} +// pub(super) fn add_pool_address(fd: RawFd, pool_addr: IpNetwork, pool_ticket: u32) { +// let mut pfioc_pooladdr = unsafe { std::mem::zeroed::() }; +// pfioc_pooladdr.ticket = pool_ticket; +// pfioc_pooladdr.addr = PoolAddr::new(pool_addr, ""); // XXX: ifname +// unsafe { +// pf_add_addr(fd, &mut pfioc_pooladdr).unwrap(); +// } +// } diff --git a/src/gateway.rs b/src/gateway.rs index 3f64ec8d..4d4a7c1d 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -615,7 +615,7 @@ impl Gateway { mod tests { use std::net::Ipv4Addr; - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "netbsd")))] use defguard_wireguard_rs::Kernel; #[cfg(target_os = "macos")] use defguard_wireguard_rs::Userspace; @@ -654,7 +654,7 @@ mod tests { .map(|peer| (peer.pubkey.clone(), peer)) .collect(); - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "netbsd"))] let wgapi = WGApi::::new("wg0".into()).unwrap(); #[cfg(not(target_os = "macos"))] let wgapi = WGApi::::new("wg0".into()).unwrap(); diff --git a/src/main.rs b/src/main.rs index c3d57d94..b2270125 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use defguard_gateway::{ config::get_config, enterprise::firewall::api::FirewallApi, error::GatewayError, execute_command, gateway::Gateway, init_syslog, server::run_server, }; -#[cfg(not(target_os = "macos"))] +#[cfg(not(any(target_os = "macos", target_os = "netbsd")))] use defguard_wireguard_rs::Kernel; use defguard_wireguard_rs::{Userspace, WGApi}; use env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}; @@ -45,12 +45,12 @@ async fn main() -> Result<(), GatewayError> { let wgapi = WGApi::::new(ifname)?; Gateway::new(config.clone(), wgapi, firewall_api)? } else { - #[cfg(not(target_os = "macos"))] + #[cfg(not(any(target_os = "macos", target_os = "netbsd")))] { let wgapi = WGApi::::new(ifname)?; Gateway::new(config.clone(), wgapi, firewall_api)? } - #[cfg(target_os = "macos")] + #[cfg(any(target_os = "macos", target_os = "netbsd"))] { eprintln!("Gateway only supports userspace WireGuard for macOS"); return Ok(()); From d786d8a1e85980d421e3cb03aa762282b418369a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Thu, 29 May 2025 14:55:29 +0200 Subject: [PATCH 09/18] PF: handle PoolAddr for NAT, redirect, etc. --- Cargo.lock | 13 +- src/enterprise/firewall/packetfilter/api.rs | 103 +++++++++ src/enterprise/firewall/packetfilter/calls.rs | 211 ++++++++++++++---- src/enterprise/firewall/packetfilter/mod.rs | 104 +-------- src/enterprise/firewall/packetfilter/rule.rs | 7 +- .../firewall/packetfilter/ticket.rs | 18 +- 6 files changed, 286 insertions(+), 170 deletions(-) create mode 100644 src/enterprise/firewall/packetfilter/api.rs diff --git a/Cargo.lock b/Cargo.lock index 67d70b01..1fecb3a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,9 +304,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -852,12 +852,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", diff --git a/src/enterprise/firewall/packetfilter/api.rs b/src/enterprise/firewall/packetfilter/api.rs new file mode 100644 index 00000000..db38b04d --- /dev/null +++ b/src/enterprise/firewall/packetfilter/api.rs @@ -0,0 +1,103 @@ +use std::os::fd::AsRawFd; + +use super::{ + calls::{pf_begin, pf_commit, pf_rollback, IocTrans, IocTransElement}, + rule::RuleSet, + ticket::get_pool_ticket, + FirewallRule, +}; +use crate::enterprise::firewall::{ + api::{FirewallApi, FirewallManagementApi}, + FirewallError, Policy, +}; + +impl FirewallManagementApi for FirewallApi { + fn setup( + &mut self, + default_policy: Policy, + _priority: Option, + ) -> Result<(), FirewallError> { + self.default_policy = default_policy; + Ok(()) + } + + /// Clean up the firewall rules. + fn cleanup(&mut self) -> Result<(), FirewallError> { + Ok(()) + } + + /// Add firewall `rules`. + fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { + let anchor = &self.anchor(); + // Begin transaction. + debug!("Begin pf transaction."); + let mut elements = [IocTransElement::new(RuleSet::Filter, anchor)]; + let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); + // This will create an anchor if it doesn't exist. + unsafe { + pf_begin(self.fd(), &mut ioc_trans)?; + } + + let ticket = elements[0].ticket; + let pool_ticket = get_pool_ticket(self.fd(), anchor)?; + + // Create first rule from the default policy. + if let Err(err) = self.add_rule_policy(ticket, pool_ticket, anchor) { + error!("Default policy rule can't be added."); + debug!("Rollback pf transaction."); + // Rule cannot be added, so rollback. + unsafe { + pf_rollback(self.fd(), &mut ioc_trans)?; + return Err(FirewallError::TransactionFailed(err.to_string())); + } + } + + for mut rule in rules { + if let Err(err) = self.add_rule(&mut rule, ticket, pool_ticket, anchor) { + error!("Firewall rule {} can't be added.", &rule.id); + debug!("Rollback pf transaction."); + // Rule cannot be added, so rollback. + unsafe { + pf_rollback(self.fd(), &mut ioc_trans)?; + return Err(FirewallError::TransactionFailed(err.to_string())); + } + } + } + + // Commit transaction. + debug!("Commit pf transaction."); + unsafe { + pf_commit(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); + } + + Ok(()) + } + + /// Set default firewall policy. + fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { + self.default_policy = policy; + Ok(()) + } + + /// Set masquerade status. + fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { + Ok(()) + } + + /// Begin rule transaction. + fn begin(&mut self) -> Result<(), FirewallError> { + // TODO: remove this no-op. + Ok(()) + } + + /// Commit rule transaction. + fn commit(&mut self) -> Result<(), FirewallError> { + // TODO: remove this no-op. + Ok(()) + } + + /// Rollback rule transaction. + fn rollback(&mut self) { + // TODO: remove this no-op. + } +} diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index d3b5a80b..5c556f62 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -2,7 +2,9 @@ use std::{ ffi::{c_char, c_int, c_long, c_uchar, c_uint, c_ulong, c_ushort, c_void}, - mem::{size_of, MaybeUninit}, + fmt, + mem::{size_of, zeroed, MaybeUninit}, + ptr, }; use ipnetwork::IpNetwork; @@ -18,7 +20,7 @@ type Addr = [u8; 16]; // Do not use u128 for the sake of alignment. type PoolHashKey = [u8; 16]; /// Equivalent to `struct pf_addr_wrap_addr_mask`. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] #[repr(C)] struct AddrMask { addr: Addr, @@ -48,16 +50,31 @@ impl From for AddrMask { } } +union VTarget { + a: AddrMask, + ifname: [u8; IFNAMSIZ], + // tblname: [u8; 32], + // rtlabelname: [u8; 32], + // rtlabel: c_uint, +} + +// const PFI_AFLAG_NETWORK: u8 = 1; +// const PFI_AFLAG_BROADCAST: u8 = 2; +// const PFI_AFLAG_PEER: u8 = 4; +// const PFI_AFLAG_MODEMASK: u8 = 7; +// const PFI_AFLAG_NOALIAS: u8 = 8; + /// Equivalent to `struct pf_addr_wrap`. /// Only the `v` part of the union, as `p` is not used in this crate. -#[derive(Debug)] #[repr(C)] struct AddrWrap { - v: AddrMask, - // unused in this crate + v: VTarget, + // Unused in this crate. p: u64, + // Determines type of field `v`. r#type: AddrType, - iflags: c_uchar, + // See PFI_AFLAG + iflags: u8, } #[derive(Debug)] @@ -71,7 +88,7 @@ pub enum AddrType { DynIftl, // PF_ADDR_TABLE = 3, Table, - // Below differs on macOS and FreeBSD. + // Values below differ on macOS and FreeBSD. // PF_ADDR_RTLABEL = 4, // RtLabel, // // PF_ADDR_URPFFAILED = 5, @@ -82,14 +99,51 @@ pub enum AddrType { impl AddrWrap { #[must_use] - pub fn new(ip_network: IpNetwork) -> Self { + fn with_network(ip_network: IpNetwork) -> Self { Self { - v: ip_network.into(), + v: VTarget { + a: ip_network.into(), + }, p: 0, r#type: AddrType::AddrMask, iflags: 0, } } + + #[must_use] + fn with_interface(ifname: &str) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + let len = ifname.len().min(IFNAMSIZ - 1); + unsafe { + (*self_ptr).v.ifname[..len].copy_from_slice(&ifname.as_bytes()[..len]); + // Probably, this is needed only for pfctl to omit displaying number of bits. + // FIXME: Fill all bytes for IPv6. + (*self_ptr).v.a.mask[..4].fill(255); + (*self_ptr).r#type = AddrType::DynIftl; + } + + unsafe { uninit.assume_init() } + } +} + +impl fmt::Debug for AddrWrap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("AddrWrap"); + match self.r#type { + AddrType::AddrMask => { + debug.field("v.a", unsafe { &self.v.a }); + } + AddrType::DynIftl => { + debug.field("v.ifname", unsafe { &self.v.ifname }); + } + _ => (), + } + debug.field("p", &self.p); + debug.field("type", &self.r#type); + debug.field("iflags", &self.iflags); + debug.finish() + } } /// Equivalent to `struct pf_rule_addr`. @@ -112,7 +166,7 @@ pub(super) struct RuleAddr { impl RuleAddr { #[must_use] pub(super) fn new(ip_network: IpNetwork, port: Port) -> Self { - let addr = AddrWrap::new(ip_network); + let addr = AddrWrap::with_network(ip_network); let from_port; let to_port; let op; @@ -145,34 +199,54 @@ impl RuleAddr { } } -// TAILQ_ENTRY #[derive(Debug)] #[repr(C)] -struct List { +struct TailQueue { + tqh_first: *mut T, + tqh_last: *mut *mut T, +} + +impl TailQueue { + fn init(&mut self) { + self.tqh_first = ptr::null_mut(); + self.tqh_last = &mut self.tqh_first; + } +} + +#[derive(Debug)] +#[repr(C)] +struct TailQueueEntry { tqe_next: *mut T, tqe_prev: *mut *mut T, } -// Equivalent to `struct pf_pooladdr`. +/// Equivalent to `struct pf_pooladdr`. #[derive(Debug)] #[repr(C)] pub struct PoolAddr { addr: AddrWrap, - entries: List, + entries: TailQueueEntry, ifname: [u8; IFNAMSIZ], kif: usize, // *mut c_void, } impl PoolAddr { #[must_use] - pub fn new(ip_network: IpNetwork, if_name: &str) -> Self { - let mut ifname = [0; IFNAMSIZ]; - let len = if_name.len().min(IFNAMSIZ - 1); - ifname[..len].copy_from_slice(&if_name.as_bytes()[..len]); + pub fn with_network(ip_network: IpNetwork) -> Self { + Self { + addr: AddrWrap::with_network(ip_network), + entries: unsafe { zeroed::>() }, + ifname: [0; IFNAMSIZ], + kif: 0, + } + } + + #[must_use] + pub fn with_interface(ifname: &str) -> Self { Self { - addr: AddrWrap::new(ip_network), - entries: unsafe { std::mem::zeroed::>() }, - ifname, + addr: AddrWrap::with_interface(ifname), + entries: unsafe { zeroed::>() }, + ifname: [0; IFNAMSIZ], kif: 0, } } @@ -197,8 +271,8 @@ pub(super) enum PoolOpts { #[derive(Debug)] #[repr(C)] pub(super) struct Pool { - list: List, - cur: *mut c_void, + list: TailQueue, + cur: *mut PoolAddr, key: PoolHashKey, counter: Addr, tblidx: c_int, @@ -207,7 +281,7 @@ pub(super) struct Pool { port_op: PortOp, pub(super) opts: PoolOpts, #[cfg(target_os = "macos")] - af: AddressFamily, // sa_family_t, + af: AddressFamily, } #[derive(Debug)] @@ -237,7 +311,7 @@ enum PortOp { impl Pool { #[must_use] - pub fn new(from_port: u16, to_port: u16) -> Self { + pub(super) fn new(from_port: u16, to_port: u16) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); unsafe { @@ -247,13 +321,31 @@ impl Pool { unsafe { uninit.assume_init() } } + + /// Insert `PoolAddr` at the end of the list. Take ownership of the given `PoolAddr`. + pub(super) fn insert_pool_addr(&mut self, mut pool_addr: PoolAddr) { + // TODO: Traverse tail queue; for now assume empty tail queue. + if !self.list.tqh_first.is_null() { + panic!("Expected one entry in PoolAddr TailQueue."); + } + self.list.tqh_first = &mut pool_addr; + self.list.tqh_last = &mut pool_addr.entries.tqe_next; + pool_addr.entries.tqe_next = ptr::null_mut(); + pool_addr.entries.tqe_prev = &mut self.list.tqh_first; + } } -#[repr(C)] -struct pf_anchor_global { - rbe_left: *mut pf_anchor, - rbe_right: *mut pf_anchor, - rbe_parent: *mut pf_anchor, +impl Drop for Pool { + // `Pool` owns the list of `PoolAddr`, so drop them here. + fn drop(&mut self) { + let mut next = self.list.tqh_first; + while !next.is_null() { + unsafe { + next = (*next).entries.tqe_next; + ptr::drop_in_place(self.list.tqh_first); + } + } + } } #[repr(C)] @@ -263,15 +355,9 @@ struct pf_anchor_node { rbe_parent: *mut pf_anchor, } -#[repr(C)] -struct pf_rulequeue { - tqh_first: *mut Rule, - tqh_last: *mut *mut Rule, -} - #[repr(C)] struct pf_ruleset_rule { - ptr: *mut pf_rulequeue, + ptr: *mut TailQueue, ptr_array: *mut *mut Rule, rcount: c_uint, rsize: c_uint, @@ -281,7 +367,7 @@ struct pf_ruleset_rule { #[repr(C)] struct pf_ruleset_rules { - queues: [pf_rulequeue; 2], + queues: [TailQueue; 2], active: pf_ruleset_rule, inactive: pf_ruleset_rule, } @@ -297,7 +383,7 @@ struct pf_ruleset { #[repr(C)] struct pf_anchor { - entry_global: pf_anchor_global, + entry_global: pf_anchor_node, entry_node: pf_anchor_node, parent: *mut pf_anchor, children: pf_anchor_node, @@ -343,7 +429,7 @@ pub(super) struct Rule { match_tagname: [c_uchar; 64], overload_tblname: [c_uchar; 32], - entries: List, + entries: TailQueueEntry, pub(super) rpool: Pool, evaluations: c_long, @@ -414,7 +500,7 @@ pub(super) struct Rule { natpass: c_uchar, keep_state: State, - af: AddressFamily, // sa_family_t + af: AddressFamily, proto: c_uchar, r#type: c_uchar, code: c_uchar, @@ -505,6 +591,8 @@ impl Rule { (*self_ptr).flags = pf_rule.tcp_flags; (*self_ptr).flagset = pf_rule.tcp_flags_set; + (*self_ptr).rpool.list.init(); + uninit.assume_init() } } @@ -604,7 +692,7 @@ pub(super) struct IocPoolAddr { impl IocPoolAddr { #[must_use] - pub fn new(anchor: &str) -> Self { + pub(super) fn new(anchor: &str) -> Self { let mut uninit = MaybeUninit::::zeroed(); let self_ptr = uninit.as_mut_ptr(); @@ -616,6 +704,18 @@ impl IocPoolAddr { unsafe { uninit.assume_init() } } + + #[must_use] + pub(super) fn with_pool_addr(addr: PoolAddr, ticket: c_uint) -> Self { + let mut uninit = MaybeUninit::::zeroed(); + let self_ptr = uninit.as_mut_ptr(); + unsafe { + (*self_ptr).ticket = ticket; + (*self_ptr).addr = addr; + } + + unsafe { uninit.assume_init() } + } } /// Equivalent to `struct pfioc_trans_pfioc_trans_e`. @@ -720,6 +820,19 @@ ioctl_readwrite!(pf_begin_addrs, b'D', 51, IocPoolAddr); // or DIOCCHANGERULE call. All other members of the structure are ignored. ioctl_readwrite!(pf_add_addr, b'D', 52, IocPoolAddr); +// DIOCGETADDRS +// Get a ticket for subsequent DIOCGETADDR calls and the number nr of pool addresses in the rule +// specified with r_action, r_num, and anchor. +ioctl_readwrite!(pf_get_addrs, b'D', 53, IocPoolAddr); + +// DIOCGETADDR +// Get the pool address addr by its number nr from the rule specified with r_action, r_num, and +// anchor using the ticket obtained through a preceding DIOCGETADDRS call. +ioctl_readwrite!(pf_get_addr, b'D', 54, IocPoolAddr); + +// DIOCCHANGEADDR +// ioctl_readwrite!(pf_change_addr, b'D', 55, IocPoolAddr); + // DIOCGETRULESETS // ioctl_readwrite!(pf_get_rulesets, b'D', 58, PFRuleset); @@ -784,27 +897,27 @@ mod tests { } #[test] - fn check_addr_wrap() { + fn check_addr_mask() { let ipnetv4 = IpNetwork::V4(Ipv4Network::new(Ipv4Addr::LOCALHOST, 8).unwrap()); - let addr_wrap = AddrWrap::new(ipnetv4); + let addr_mask = AddrMask::from(ipnetv4); assert_eq!( - addr_wrap.v.addr, + addr_mask.addr, [127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ); assert_eq!( - addr_wrap.v.mask, + addr_mask.mask, [255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ); let ipv6 = IpNetwork::V6(Ipv6Network::new(Ipv6Addr::LOCALHOST, 32).unwrap()); - let addr_wrap = AddrWrap::new(ipv6); + let addr_wrap = AddrMask::from(ipv6); assert_eq!( - addr_wrap.v.addr, + addr_wrap.addr, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] ); assert_eq!( - addr_wrap.v.mask, + addr_wrap.mask, [255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ); } diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index 9e4a3966..ba434477 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -19,21 +19,12 @@ mod ticket; use std::os::fd::{AsRawFd, RawFd}; -use calls::pf_rollback; use rule::PacketFilterRule; -use self::{ - calls::{pf_add_rule, pf_begin, pf_commit, Change, IocRule, IocTrans, IocTransElement, Rule}, - rule::RuleSet, - ticket::get_pool_ticket, -}; +use self::calls::{pf_add_rule, Change, IocRule, Rule}; +use super::{api::FirewallApi, FirewallError, FirewallRule}; use crate::enterprise::firewall::Port; -use super::{ - api::{FirewallApi, FirewallManagementApi}, - FirewallError, FirewallRule, Policy, -}; - const ANCHOR_PREFIX: &str = "defguard/"; impl FirewallApi { @@ -94,93 +85,4 @@ impl FirewallApi { } #[cfg(not(test))] -impl FirewallManagementApi for FirewallApi { - fn setup( - &mut self, - default_policy: Policy, - _priority: Option, - ) -> Result<(), FirewallError> { - self.default_policy = default_policy; - Ok(()) - } - - /// Clean up the firewall rules. - fn cleanup(&mut self) -> Result<(), FirewallError> { - Ok(()) - } - - /// Add firewall `rules`. - fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError> { - let anchor = &self.anchor(); - // Begin transaction. - debug!("Begin pf transaction."); - let mut elements = [IocTransElement::new(RuleSet::Filter, anchor)]; - let mut ioc_trans = IocTrans::new(elements.as_mut_slice()); - // This will create an anchor if it doesn't exist. - unsafe { - pf_begin(self.fd(), &mut ioc_trans)?; - } - - let ticket = elements[0].ticket; - let pool_ticket = get_pool_ticket(self.fd(), anchor)?; - - // Create first rule from the default policy. - if let Err(err) = self.add_rule_policy(ticket, pool_ticket, anchor) { - error!("Default policy rule can't be added."); - debug!("Rollback pf transaction."); - // Rule cannot be added, so rollback. - unsafe { - pf_rollback(self.fd(), &mut ioc_trans)?; - return Err(FirewallError::TransactionFailed(err.to_string())); - } - } - - for mut rule in rules { - if let Err(err) = self.add_rule(&mut rule, ticket, pool_ticket, anchor) { - error!("Firewall rule {} can't be added.", &rule.id); - debug!("Rollback pf transaction."); - // Rule cannot be added, so rollback. - unsafe { - pf_rollback(self.fd(), &mut ioc_trans)?; - return Err(FirewallError::TransactionFailed(err.to_string())); - } - } - } - - // Commit transaction. - debug!("Commit pf transaction."); - unsafe { - pf_commit(self.file.as_raw_fd(), &mut ioc_trans).unwrap(); - } - - Ok(()) - } - - /// Set default firewall policy. - fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { - self.default_policy = policy; - Ok(()) - } - - /// Set masquerade status. - fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { - Ok(()) - } - - /// Begin rule transaction. - fn begin(&mut self) -> Result<(), FirewallError> { - // TODO: remove this no-op. - Ok(()) - } - - /// Commit rule transaction. - fn commit(&mut self) -> Result<(), FirewallError> { - // TODO: remove this no-op. - Ok(()) - } - - /// Rollback rule transaction. - fn rollback(&mut self) { - // TODO: remove this no-op. - } -} +mod api; diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index 311c74b0..97b7aaf2 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -1,10 +1,7 @@ -use std::{ - fmt, - net::{IpAddr, Ipv4Addr}, -}; +use std::fmt; use ipnetwork::IpNetwork; -use libc::{dirfd, AF_INET, AF_INET6, AF_UNSPEC}; +use libc::{AF_INET, AF_INET6, AF_UNSPEC}; use super::{FirewallRule, Port}; use crate::enterprise::firewall::{Address, Policy, Protocol}; diff --git a/src/enterprise/firewall/packetfilter/ticket.rs b/src/enterprise/firewall/packetfilter/ticket.rs index c02032b3..49f2945a 100644 --- a/src/enterprise/firewall/packetfilter/ticket.rs +++ b/src/enterprise/firewall/packetfilter/ticket.rs @@ -31,12 +31,12 @@ pub(super) fn get_pool_ticket(fd: RawFd, anchor: &str) -> Result() }; -// pfioc_pooladdr.ticket = pool_ticket; -// pfioc_pooladdr.addr = PoolAddr::new(pool_addr, ""); // XXX: ifname -// unsafe { -// pf_add_addr(fd, &mut pfioc_pooladdr).unwrap(); -// } -// } +/// Add pool address using the pool ticket previously obtained via `get_pool_ticket()` +pub fn add_pool_address(fd: RawFd, ticket: u32, ifname: &str) -> Result<(), FirewallError> { + let mut pfioc_pooladdr = IocPoolAddr::with_pool_addr(PoolAddr::with_interface(ifname), ticket); + unsafe { + pf_add_addr(fd, &mut pfioc_pooladdr)?; + } + + Ok(()) +} From 9ea12dc0130cc957f798c6b9f00d1c88cb24f0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Thu, 29 May 2025 20:31:17 +0200 Subject: [PATCH 10/18] Add anchors in OPNsense plugin --- .../etc/inc/plugins.inc.d/defguardgateway.inc | 19 +++++++++++++++++++ src/enterprise/firewall/packetfilter/api.rs | 2 +- src/enterprise/firewall/packetfilter/mod.rs | 4 ++-- .../firewall/packetfilter/ticket.rs | 2 -- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc b/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc index 1c1b8a6f..5de53ddc 100644 --- a/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc +++ b/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc @@ -59,3 +59,22 @@ function defguardgateway_devices() return $devices; } + +function defguardgateway_enabled() +{ + global $config; + + return isset($config['OPNsense']['defguardgateway']['general']['Enabled']) && + $config['OPNsense']['defguardgateway']['general']['Enabled'] == 1; +} + +function defguardgateway_firewall($fw) +{ + if (!defguardgateway_enabled()) { + return; + } + + $fw->registerAnchor('defguard/*', 'nat'); + $fw->registerAnchor('defguard/*', 'rdr'); + $fw->registerAnchor('defguard/*', 'fw'); +} diff --git a/src/enterprise/firewall/packetfilter/api.rs b/src/enterprise/firewall/packetfilter/api.rs index db38b04d..af3e47f0 100644 --- a/src/enterprise/firewall/packetfilter/api.rs +++ b/src/enterprise/firewall/packetfilter/api.rs @@ -80,7 +80,7 @@ impl FirewallManagementApi for FirewallApi { } /// Set masquerade status. - fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { + fn set_masquerade_status(&mut self, _enabled: bool) -> Result<(), FirewallError> { Ok(()) } diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index ba434477..3a8c9d26 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -25,12 +25,12 @@ use self::calls::{pf_add_rule, Change, IocRule, Rule}; use super::{api::FirewallApi, FirewallError, FirewallRule}; use crate::enterprise::firewall::Port; -const ANCHOR_PREFIX: &str = "defguard/"; +const ANCHOR_PREFIX: &str = "defguard"; impl FirewallApi { /// Construct anchor name based on prefix and network interface name. fn anchor(&self) -> String { - ANCHOR_PREFIX.to_owned() + &self.ifname + ANCHOR_PREFIX.to_owned() // + &self.ifname } /// Return raw file descriptor to Packet Filter device. diff --git a/src/enterprise/firewall/packetfilter/ticket.rs b/src/enterprise/firewall/packetfilter/ticket.rs index 49f2945a..816c083a 100644 --- a/src/enterprise/firewall/packetfilter/ticket.rs +++ b/src/enterprise/firewall/packetfilter/ticket.rs @@ -1,7 +1,5 @@ use std::os::fd::RawFd; -use ipnetwork::IpNetwork; - use super::{ calls::{pf_add_addr, pf_begin_addrs, pf_change_rule, Change, IocPoolAddr, IocRule, PoolAddr}, rule::Action, From a7f017f69e823aac66c7fbb7bc6d4a3f02ccf5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 14:23:28 +0200 Subject: [PATCH 11/18] Final touches --- Cargo.lock | 86 +------------------ Cargo.toml | 8 +- build.rs | 2 +- .../etc/inc/plugins.inc.d/defguardgateway.inc | 6 +- src/enterprise/firewall/packetfilter/mod.rs | 6 +- src/enterprise/firewall/packetfilter/rule.rs | 28 +++--- src/gateway.rs | 5 +- 7 files changed, 31 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fecb3a8..708e8f3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,28 +82,6 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-trait" version = "0.1.88" @@ -127,40 +105,13 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core 0.5.2", + "axum-core", "axum-macros", "bytes", "form_urlencoded", @@ -171,7 +122,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -189,26 +140,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-core" version = "0.5.2" @@ -447,7 +378,7 @@ dependencies = [ name = "defguard-gateway" version = "1.3.1" dependencies = [ - "axum 0.8.4", + "axum", "base64", "clap", "defguard_wireguard_rs", @@ -460,7 +391,6 @@ dependencies = [ "nftnl", "nix", "prost", - "prost-build", "serde", "syslog", "thiserror 2.0.12", @@ -1113,12 +1043,6 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -2094,13 +2018,10 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ - "async-stream", "async-trait", - "axum 0.7.9", "base64", "bytes", "flate2", - "h2", "http", "http-body", "http-body-util", @@ -2112,7 +2033,6 @@ dependencies = [ "prost", "rustls-native-certs", "rustls-pemfile", - "socket2", "tokio", "tokio-rustls", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 20e10eb9..91886800 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,12 @@ prost = "0.13" serde = { version = "1.0", features = ["derive"] } syslog = "7.0" thiserror = "2.0" -tonic = { version = "0.12", features = ["gzip", "tls", "tls-native-roots"] } +tonic = { version = "0.12", default-features = false, features = [ + "codegen", + "gzip", + "prost", + "tls-native-roots", +] } tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } tokio-stream = { version = "0.1", features = [] } toml = { version = "0.8", default-features = false, features = ["parse"] } @@ -35,7 +40,6 @@ tokio = { version = "1", features = ["io-std", "io-util"] } x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"] } [build-dependencies] -prost-build = { version = "0.13" } tonic-build = { version = "0.12" } vergen-git2 = { version = "1.0", features = ["build"] } diff --git a/build.rs b/build.rs index 744ed848..74db20ad 100644 --- a/build.rs +++ b/build.rs @@ -6,7 +6,7 @@ fn main() -> Result<(), Box> { Emitter::default().add_instructions(&git2)?.emit()?; // compiling protos using path on build time - let mut config = prost_build::Config::new(); + let mut config = tonic_build::Config::new(); // enable optional fields config.protoc_arg("--experimental_allow_proto3_optional"); tonic_build::configure().compile_protos_with_config( diff --git a/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc b/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc index 5de53ddc..e15d494d 100644 --- a/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc +++ b/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc @@ -74,7 +74,7 @@ function defguardgateway_firewall($fw) return; } - $fw->registerAnchor('defguard/*', 'nat'); - $fw->registerAnchor('defguard/*', 'rdr'); - $fw->registerAnchor('defguard/*', 'fw'); + // $fw->registerAnchor('defguard/*', 'nat', 1, 'head'); + // $fw->registerAnchor('defguard/*', 'rdr', 1, 'head'); + $fw->registerAnchor('defguard/*', 'fw', 1, 'head', true); } diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index 3a8c9d26..dab59579 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -25,12 +25,12 @@ use self::calls::{pf_add_rule, Change, IocRule, Rule}; use super::{api::FirewallApi, FirewallError, FirewallRule}; use crate::enterprise::firewall::Port; -const ANCHOR_PREFIX: &str = "defguard"; +const ANCHOR_PREFIX: &str = "defguard/"; impl FirewallApi { /// Construct anchor name based on prefix and network interface name. fn anchor(&self) -> String { - ANCHOR_PREFIX.to_owned() // + &self.ifname + ANCHOR_PREFIX.to_owned() + &self.ifname } /// Return raw file descriptor to Packet Filter device. @@ -45,7 +45,6 @@ impl FirewallApi { anchor: &str, ) -> Result<(), FirewallError> { let rule = PacketFilterRule::for_policy(self.default_policy, &self.ifname); - warn!("==> {rule}"); let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); ioc.ticket = ticket; ioc.pool_ticket = pool_ticket; @@ -69,7 +68,6 @@ impl FirewallApi { let rules = PacketFilterRule::from_firewall_rule(&self.ifname, rule); for rule in rules { - warn!("--> {rule}"); let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); ioc.action = Change::None; ioc.ticket = ticket; diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index 97b7aaf2..ff8497fc 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -190,9 +190,9 @@ impl PacketFilterRule { /// Default rule for policy. #[must_use] pub(super) fn for_policy(policy: Policy, ifname: &str) -> Self { - let action = match policy { - Policy::Allow => Action::Pass, - Policy::Deny => Action::Drop, + let (action, state) = match policy { + Policy::Allow => (Action::Pass, State::Normal), + Policy::Deny => (Action::Drop, State::None), }; Self { from: None, @@ -202,12 +202,12 @@ impl PacketFilterRule { action, direction: Direction::In, quick: false, - log: 0, - state: State::None, + log: PF_LOG, + state, interface: Some(ifname.to_owned()), proto: Protocol::Any, - tcp_flags: 0, - tcp_flags_set: 0, + tcp_flags: TH_SYN, + tcp_flags_set: TH_SYN | TH_ACK, label: None, } } @@ -228,9 +228,9 @@ impl PacketFilterRule { /// Expand `FirewallRule` into a set of `PacketFilterRule`s. pub(super) fn from_firewall_rule(ifname: &str, fr: &mut FirewallRule) -> Vec { let mut rules = Vec::new(); - let action = match fr.verdict { - Policy::Allow => Action::Pass, - Policy::Deny => Action::Drop, + let (action, state) = match fr.verdict { + Policy::Allow => (Action::Pass, State::Normal), + Policy::Deny => (Action::Drop, State::None), }; let mut from_addrs = Vec::new(); @@ -283,13 +283,11 @@ impl PacketFilterRule { to: *to, to_port: *to_port, action, - direction: Direction::Out, + direction: Direction::In, // Enable quick to match NFTables behaviour. quick: true, - // Disable logging. - log: 0, - // Keeping state is the default. - state: State::Normal, + log: PF_LOG, + state, interface: Some(ifname.to_owned()), proto: *proto, // For stateful connections, the default is flags S/SA. diff --git a/src/gateway.rs b/src/gateway.rs index 4d4a7c1d..406828d0 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -445,15 +445,16 @@ impl Gateway { ) -> Result>, GatewayError> { debug!("Preparing gRPC client configuration"); + let tls = ClientTlsConfig::new(); // Use CA if provided, otherwise load certificates from system. let tls = if let Some(ca) = &config.grpc_ca { let ca = read_to_string(ca).map_err(|err| { error!("Failed to read CA file: {err}"); GatewayError::InvalidCaFile })?; - ClientTlsConfig::new().ca_certificate(Certificate::from_pem(ca)) + tls.ca_certificate(Certificate::from_pem(ca)) } else { - ClientTlsConfig::new().with_native_roots() + tls.with_enabled_roots() }; let endpoint = Endpoint::from_shared(config.grpc_url.clone())? .http2_keep_alive_interval(TEN_SECS) From d77cd6e59797366f377bbab41e2ca861d90fdd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 15:14:36 +0200 Subject: [PATCH 12/18] Cleanup --- src/enterprise/firewall/packetfilter/api.rs | 3 +- src/enterprise/firewall/packetfilter/mod.rs | 14 ++++++- .../firewall/packetfilter/ticket.rs | 40 ------------------- 3 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 src/enterprise/firewall/packetfilter/ticket.rs diff --git a/src/enterprise/firewall/packetfilter/api.rs b/src/enterprise/firewall/packetfilter/api.rs index af3e47f0..451a6fef 100644 --- a/src/enterprise/firewall/packetfilter/api.rs +++ b/src/enterprise/firewall/packetfilter/api.rs @@ -3,7 +3,6 @@ use std::os::fd::AsRawFd; use super::{ calls::{pf_begin, pf_commit, pf_rollback, IocTrans, IocTransElement}, rule::RuleSet, - ticket::get_pool_ticket, FirewallRule, }; use crate::enterprise::firewall::{ @@ -39,7 +38,7 @@ impl FirewallManagementApi for FirewallApi { } let ticket = elements[0].ticket; - let pool_ticket = get_pool_ticket(self.fd(), anchor)?; + let pool_ticket = self.get_pool_ticket(anchor)?; // Create first rule from the default policy. if let Err(err) = self.add_rule_policy(ticket, pool_ticket, anchor) { diff --git a/src/enterprise/firewall/packetfilter/mod.rs b/src/enterprise/firewall/packetfilter/mod.rs index dab59579..81cccdce 100644 --- a/src/enterprise/firewall/packetfilter/mod.rs +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -15,10 +15,10 @@ mod calls; mod rule; -mod ticket; use std::os::fd::{AsRawFd, RawFd}; +use calls::{pf_begin_addrs, IocPoolAddr}; use rule::PacketFilterRule; use self::calls::{pf_add_rule, Change, IocRule, Rule}; @@ -38,6 +38,16 @@ impl FirewallApi { self.file.as_raw_fd() } + fn get_pool_ticket(&self, anchor: &str) -> Result { + let mut ioc = IocPoolAddr::new(anchor); + + unsafe { + pf_begin_addrs(self.fd(), &mut ioc)?; + } + + Ok(ioc.ticket) + } + fn add_rule_policy( &mut self, ticket: u32, @@ -64,7 +74,7 @@ impl FirewallApi { pool_ticket: u32, anchor: &str, ) -> Result<(), FirewallError> { - warn!("add_rule {rule:?}"); + debug!("add_rule {rule:?}"); let rules = PacketFilterRule::from_firewall_rule(&self.ifname, rule); for rule in rules { diff --git a/src/enterprise/firewall/packetfilter/ticket.rs b/src/enterprise/firewall/packetfilter/ticket.rs deleted file mode 100644 index 816c083a..00000000 --- a/src/enterprise/firewall/packetfilter/ticket.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::os::fd::RawFd; - -use super::{ - calls::{pf_add_addr, pf_begin_addrs, pf_change_rule, Change, IocPoolAddr, IocRule, PoolAddr}, - rule::Action, -}; -use crate::enterprise::firewall::FirewallError; - -pub(super) fn get_ticket(fd: RawFd, anchor: &str, kind: Action) -> Result { - let mut pfioc_rule = IocRule::new(anchor); - - pfioc_rule.action = Change::GetTicket; - pfioc_rule.rule.action = kind; - - unsafe { - pf_change_rule(fd, &mut pfioc_rule)?; - } - - Ok(pfioc_rule.ticket) -} - -pub(super) fn get_pool_ticket(fd: RawFd, anchor: &str) -> Result { - let mut ioc = IocPoolAddr::new(anchor); - - unsafe { - pf_begin_addrs(fd, &mut ioc)?; - } - - Ok(ioc.ticket) -} - -/// Add pool address using the pool ticket previously obtained via `get_pool_ticket()` -pub fn add_pool_address(fd: RawFd, ticket: u32, ifname: &str) -> Result<(), FirewallError> { - let mut pfioc_pooladdr = IocPoolAddr::with_pool_addr(PoolAddr::with_interface(ifname), ticket); - unsafe { - pf_add_addr(fd, &mut pfioc_pooladdr)?; - } - - Ok(()) -} From 698aa186da5ab14138d8d31e53a579b870996db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 15:47:45 +0200 Subject: [PATCH 13/18] Switch to wireguard-rs 0.7.3; more cleanups --- Cargo.lock | 5 +- Cargo.toml | 3 +- src/enterprise/firewall/api.rs | 6 -- src/enterprise/firewall/dummy/mod.rs | 6 -- src/enterprise/firewall/iprange.rs | 1 + src/enterprise/firewall/mod.rs | 2 + src/enterprise/firewall/nftables/mod.rs | 63 +++++++++---------- src/enterprise/firewall/nftables/netfilter.rs | 38 +++++------ src/enterprise/firewall/packetfilter/api.rs | 11 ---- src/enterprise/firewall/packetfilter/calls.rs | 24 +++---- src/enterprise/firewall/packetfilter/rule.rs | 13 +++- 11 files changed, 77 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 708e8f3c..4742dc67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,9 +212,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.24" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "jobserver", "libc", @@ -406,6 +406,7 @@ dependencies = [ [[package]] name = "defguard_wireguard_rs" version = "0.7.3" +source = "git+https://github.com/DefGuard/wireguard-rs.git?rev=v0.7.3#69f1ff7064240f5f2ddbab242db0e8271733bf46" dependencies = [ "base64", "libc", diff --git a/Cargo.toml b/Cargo.toml index 91886800..5339a019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,7 @@ edition = "2021" axum = { version = "0.8", features = ["macros"] } base64 = "0.22" clap = { version = "4.5", features = ["derive", "env"] } -# defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.7.3" } -defguard_wireguard_rs = { path = "../wireguard-rs" } +defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.7.3" } env_logger = "0.11" gethostname = "1.0" ipnetwork = "0.21" diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index e301de5b..dc3506a2 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -44,9 +44,6 @@ pub(crate) trait FirewallManagementApi { /// Add fireall `rules`. fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError>; - /// Set default firewall policy. - fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError>; - /// Set masquerade status. fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError>; @@ -55,7 +52,4 @@ pub(crate) trait FirewallManagementApi { /// Commit rule transaction. fn commit(&mut self) -> Result<(), FirewallError>; - - /// Rollback rule transaction. - fn rollback(&mut self); } diff --git a/src/enterprise/firewall/dummy/mod.rs b/src/enterprise/firewall/dummy/mod.rs index 38d8437c..9129f91f 100644 --- a/src/enterprise/firewall/dummy/mod.rs +++ b/src/enterprise/firewall/dummy/mod.rs @@ -16,10 +16,6 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - fn set_firewall_default_policy(&mut self, _policy: Policy) -> Result<(), FirewallError> { - Ok(()) - } - fn set_masquerade_status(&mut self, _enabled: bool) -> Result<(), FirewallError> { Ok(()) } @@ -32,8 +28,6 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - fn rollback(&mut self) {} - fn commit(&mut self) -> Result<(), FirewallError> { Ok(()) } diff --git a/src/enterprise/firewall/iprange.rs b/src/enterprise/firewall/iprange.rs index aa793043..33eab39d 100644 --- a/src/enterprise/firewall/iprange.rs +++ b/src/enterprise/firewall/iprange.rs @@ -31,6 +31,7 @@ impl fmt::Display for IpAddrRangeError { } } +#[allow(dead_code)] impl IpAddrRange { pub fn new(start: IpAddr, end: IpAddr) -> Result { if start > end { diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index 4c8b1a7e..d1c9b7ce 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -120,10 +120,12 @@ pub(crate) enum Protocol { Icmp = libc::IPPROTO_ICMP as u8, Tcp = libc::IPPROTO_TCP as u8, Udp = libc::IPPROTO_UDP as u8, + #[allow(dead_code)] IcmpV6 = libc::IPPROTO_ICMPV6 as u8, } impl Protocol { + #[cfg(target_os = "linux")] #[must_use] pub(crate) fn supports_ports(self) -> bool { matches!(self, Protocol::Tcp | Protocol::Udp) diff --git a/src/enterprise/firewall/nftables/mod.rs b/src/enterprise/firewall/nftables/mod.rs index 55ccca82..2db6c0eb 100644 --- a/src/enterprise/firewall/nftables/mod.rs +++ b/src/enterprise/firewall/nftables/mod.rs @@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; use netfilter::{ allow_established_traffic, apply_filter_rules, drop_table, ignore_unrelated_traffic, - init_firewall, send_batch, set_default_policy, set_masq, + init_firewall, send_batch, set_masq, }; use nftnl::Batch; @@ -20,7 +20,7 @@ pub fn get_set_id() -> u32 { } #[derive(Debug, Default)] -pub enum State { +enum State { #[default] Established, Invalid, @@ -29,25 +29,25 @@ pub enum State { } #[derive(Debug, Default)] -pub struct FilterRule<'a> { - pub src_ips: &'a [Address], - pub dest_ips: &'a [Address], - pub src_ports: &'a [Port], - pub dest_ports: &'a [Port], - pub protocols: Vec, - pub oifname: Option, - pub iifname: Option, - pub action: Policy, - pub states: Vec, - pub counter: bool, +struct FilterRule<'a> { + src_ips: &'a [Address], + dest_ips: &'a [Address], + // src_ports: &'a [Port], + dest_ports: &'a [Port], + protocols: Vec, + oifname: Option, + iifname: Option, + action: Policy, + states: Vec, + counter: bool, // The ID of the associated Defguard rule. // The filter rules may not always be a 1:1 representation of the Defguard rules, so // this value helps to keep track of them. - pub defguard_rule_id: i64, - pub v4: bool, - pub comment: Option, - pub negated_oifname: bool, - pub negated_iifname: bool, + defguard_rule_id: i64, + v4: bool, + comment: Option, + negated_oifname: bool, + negated_iifname: bool, } impl FirewallApi { @@ -193,17 +193,17 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - /// Allows for changing the default policy of the firewall. - fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { - debug!("Setting default firewall policy to: {policy:?}"); - if let Some(batch) = &mut self.batch { - set_default_policy(policy, batch, &self.ifname)?; - } else { - return Err(FirewallError::TransactionNotStarted); - } - debug!("Set firewall default policy to {policy:?}"); - Ok(()) - } + // Allows for changing the default policy of the firewall. + // fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { + // debug!("Setting default firewall policy to: {policy:?}"); + // if let Some(batch) = &mut self.batch { + // set_default_policy(policy, batch, &self.ifname)?; + // } else { + // return Err(FirewallError::TransactionNotStarted); + // } + // debug!("Set firewall default policy to {policy:?}"); + // Ok(()) + // } /// Allows for changing the masquerade status of the firewall. fn set_masquerade_status(&mut self, enabled: bool) -> Result<(), FirewallError> { @@ -242,11 +242,6 @@ impl FirewallManagementApi for FirewallApi { } } - fn rollback(&mut self) { - self.batch = None; - debug!("Firewall transaction has been rolled back.") - } - /// Apply whole firewall configuration and send it in one go to the kernel. fn commit(&mut self) -> Result<(), FirewallError> { if let Some(batch) = self.batch.take() { diff --git a/src/enterprise/firewall/nftables/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs index e9b7d4a6..d28aeb34 100644 --- a/src/enterprise/firewall/nftables/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -625,24 +625,24 @@ pub(crate) fn set_masq( Ok(()) } -pub(crate) fn set_default_policy( - policy: Policy, - batch: &mut Batch, - ifname: &str, -) -> Result<(), FirewallError> { - let table = Tables::Defguard(ProtoFamily::Inet).to_table(ifname); - batch.add(&table, nftnl::MsgType::Add); - - let mut forward_chain = Chains::Forward.to_chain(&table); - forward_chain.set_policy(if policy == Policy::Allow { - nftnl::Policy::Accept - } else { - nftnl::Policy::Drop - }); - batch.add(&forward_chain, nftnl::MsgType::Add); - - Ok(()) -} +// pub(super) fn set_default_policy( +// policy: Policy, +// batch: &mut Batch, +// ifname: &str, +// ) -> Result<(), FirewallError> { +// let table = Tables::Defguard(ProtoFamily::Inet).to_table(ifname); +// batch.add(&table, nftnl::MsgType::Add); + +// let mut forward_chain = Chains::Forward.to_chain(&table); +// forward_chain.set_policy(if policy == Policy::Allow { +// nftnl::Policy::Accept +// } else { +// nftnl::Policy::Drop +// }); +// batch.add(&forward_chain, nftnl::MsgType::Add); + +// Ok(()) +// } pub(crate) fn allow_established_traffic( batch: &mut Batch, @@ -741,7 +741,7 @@ impl Chains { } } -pub(crate) fn apply_filter_rules( +pub(super) fn apply_filter_rules( rules: Vec, batch: &mut Batch, ifname: &str, diff --git a/src/enterprise/firewall/packetfilter/api.rs b/src/enterprise/firewall/packetfilter/api.rs index 451a6fef..baad67cd 100644 --- a/src/enterprise/firewall/packetfilter/api.rs +++ b/src/enterprise/firewall/packetfilter/api.rs @@ -72,12 +72,6 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - /// Set default firewall policy. - fn set_firewall_default_policy(&mut self, policy: Policy) -> Result<(), FirewallError> { - self.default_policy = policy; - Ok(()) - } - /// Set masquerade status. fn set_masquerade_status(&mut self, _enabled: bool) -> Result<(), FirewallError> { Ok(()) @@ -94,9 +88,4 @@ impl FirewallManagementApi for FirewallApi { // TODO: remove this no-op. Ok(()) } - - /// Rollback rule transaction. - fn rollback(&mut self) { - // TODO: remove this no-op. - } } diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs index 5c556f62..e81b7312 100644 --- a/src/enterprise/firewall/packetfilter/calls.rs +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -77,6 +77,7 @@ struct AddrWrap { iflags: u8, } +#[allow(dead_code)] #[derive(Debug)] #[repr(u8)] pub enum AddrType { @@ -110,6 +111,7 @@ impl AddrWrap { } } + #[allow(dead_code)] #[must_use] fn with_interface(ifname: &str) -> Self { let mut uninit = MaybeUninit::::zeroed(); @@ -231,6 +233,7 @@ pub struct PoolAddr { } impl PoolAddr { + #[allow(dead_code)] #[must_use] pub fn with_network(ip_network: IpNetwork) -> Self { Self { @@ -241,6 +244,7 @@ impl PoolAddr { } } + #[allow(dead_code)] #[must_use] pub fn with_interface(ifname: &str) -> Self { Self { @@ -252,6 +256,7 @@ impl PoolAddr { } } +#[allow(dead_code)] #[derive(Debug)] #[repr(u8)] pub(super) enum PoolOpts { @@ -284,6 +289,7 @@ pub(super) struct Pool { af: AddressFamily, } +#[allow(dead_code)] #[derive(Debug)] #[repr(u8)] enum PortOp { @@ -309,6 +315,7 @@ enum PortOp { Range = 9, // ((p >= a1) && (p <= a2)) } +#[allow(dead_code)] impl Pool { #[must_use] pub(super) fn new(from_port: u16, to_port: u16) -> Self { @@ -599,6 +606,7 @@ impl Rule { } /// Equivalent to PF_CHANGE_... enum. +#[allow(dead_code)] #[repr(u32)] pub(crate) enum Change { // PF_CHANGE_NONE = 0 @@ -618,6 +626,7 @@ pub(crate) enum Change { } /// Rule flags, equivalent to PFRULE_... +#[allow(dead_code)] #[repr(u32)] pub(crate) enum RuleFlag { Drop = 0, @@ -646,20 +655,6 @@ pub(super) struct IocRule { } impl IocRule { - #[must_use] - pub(super) fn new(anchor: &str) -> Self { - let mut uninit = MaybeUninit::::zeroed(); - let self_ptr = uninit.as_mut_ptr(); - - // Copy anchor name. - let len = anchor.len().min(MAXPATHLEN - 1); - unsafe { - (*self_ptr).anchor[..len].copy_from_slice(&anchor.as_bytes()[..len]); - } - - unsafe { uninit.assume_init() } - } - #[must_use] pub(super) fn with_rule(anchor: &str, rule: Rule) -> Self { let mut uninit = MaybeUninit::::zeroed(); @@ -705,6 +700,7 @@ impl IocPoolAddr { unsafe { uninit.assume_init() } } + #[allow(dead_code)] #[must_use] pub(super) fn with_pool_addr(addr: PoolAddr, ticket: c_uint) -> Self { let mut uninit = MaybeUninit::::zeroed(); diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index ff8497fc..350d44fc 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -7,6 +7,7 @@ use super::{FirewallRule, Port}; use crate::enterprise::firewall::{Address, Policy, Protocol}; /// Packet filter rule action. +#[allow(dead_code)] #[derive(Clone, Copy, Debug)] #[repr(u8)] pub enum Action { @@ -56,6 +57,7 @@ impl fmt::Display for Action { } } +#[allow(dead_code)] #[derive(Clone, Copy, Debug)] #[repr(u8)] pub(super) enum AddressFamily { @@ -65,6 +67,7 @@ pub(super) enum AddressFamily { } /// Packet filter rule direction. +#[allow(dead_code)] #[derive(Clone, Copy, Debug)] #[repr(u8)] pub enum Direction { @@ -89,7 +92,7 @@ impl fmt::Display for Direction { } const PF_LOG: u8 = 0x01; -const PF_LOG_ALL: u8 = 0x02; +// const PF_LOG_ALL: u8 = 0x02; // const PF_LOG_SOCKET_LOOKUP: u8 = 0x04; // #[cfg(target_os = "freebsd")] // const PF_LOG_FORCE: u8 = 0x08; @@ -97,6 +100,7 @@ const PF_LOG_ALL: u8 = 0x02; // const PF_LOG_MATCHES: u8 = 0x10; /// Equivalent to `PF_RULESET_...`. +#[allow(dead_code)] #[derive(Clone, Copy, Debug)] #[repr(i32)] pub enum RuleSet { @@ -119,6 +123,7 @@ pub enum RuleSet { } // Equivalent to `PF_STATE_...`. +#[allow(dead_code)] #[derive(Clone, Copy, Debug)] #[repr(u8)] pub enum State { @@ -147,20 +152,26 @@ impl fmt::Display for State { /// TCP flags as defined in `netinet/tcp.h`. /// Final: Set on the last segment. +#[allow(dead_code)] const TH_FIN: u8 = 0x01; /// Synchronization: New conn with dst port. const TH_SYN: u8 = 0x02; /// Reset: Announce to peer conn terminated. +#[allow(dead_code)] const TH_RST: u8 = 0x04; /// Push: Immediately send, don't buffer seg. +#[allow(dead_code)] const TH_PUSH: u8 = 0x08; /// Acknowledge: Part of connection establish. const TH_ACK: u8 = 0x10; /// Urgent: send special marked segment now. +#[allow(dead_code)] const TH_URG: u8 = 0x20; /// ECN Echo. +#[allow(dead_code)] const TH_ECE: u8 = 0x40; /// Congestion Window Reduced. +#[allow(dead_code)] const TH_CWR: u8 = 0x80; #[derive(Debug)] From de9c0d246e58fa2063e3d652f6a34158378858ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 15:51:12 +0200 Subject: [PATCH 14/18] Fix examples --- Cargo.lock | 85 ++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 11 ++++-- examples/server.rs | 15 +++----- 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4742dc67..bcbb1767 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,28 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -105,13 +127,40 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ - "axum-core", + "axum-core 0.5.2", "axum-macros", "bytes", "form_urlencoded", @@ -122,7 +171,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -140,6 +189,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.5.2" @@ -378,7 +447,7 @@ dependencies = [ name = "defguard-gateway" version = "1.3.1" dependencies = [ - "axum", + "axum 0.8.4", "base64", "clap", "defguard_wireguard_rs", @@ -1044,6 +1113,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" @@ -2019,10 +2094,13 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ + "async-stream", "async-trait", + "axum 0.7.9", "base64", "bytes", "flate2", + "h2", "http", "http-body", "http-body-util", @@ -2034,6 +2112,7 @@ dependencies = [ "prost", "rustls-native-certs", "rustls-pemfile", + "socket2", "tokio", "tokio-rustls", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 5339a019..6f4f452d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,15 +17,15 @@ prost = "0.13" serde = { version = "1.0", features = ["derive"] } syslog = "7.0" thiserror = "2.0" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } +tokio-stream = { version = "0.1", features = [] } +toml = { version = "0.8", default-features = false, features = ["parse"] } tonic = { version = "0.12", default-features = false, features = [ "codegen", "gzip", "prost", "tls-native-roots", ] } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] } -tokio-stream = { version = "0.1", features = [] } -toml = { version = "0.8", default-features = false, features = ["parse"] } [target.'cfg(target_os = "linux")'.dependencies] nftnl = { git = "https://github.com/DefGuard/nftnl-rs.git", rev = "1a1147271f43b9d7182a114bb056a5224c35d38f" } @@ -36,6 +36,11 @@ nix = { version = "0.30", default-features = false, features = ["ioctl"] } [dev-dependencies] tokio = { version = "1", features = ["io-std", "io-util"] } +tonic = { version = "0.12", default-features = false, features = [ + "codegen", + "prost", + "transport", +] } x25519-dalek = { version = "2.0", features = ["getrandom", "static_secrets"] } [build-dependencies] diff --git a/examples/server.rs b/examples/server.rs index 34a9df9f..641e6b59 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -66,20 +66,15 @@ impl From<&HostConfig> for proto::gateway::Configuration { .host .private_key .as_ref() - .map(|key| key.to_string()) + .map(ToString::to_string) .unwrap_or_default(), addresses: host_config .addresses .iter() - .map(|addr| addr.to_string()) - .collect(), - port: host_config.host.listen_port as u32, - peers: host_config - .host - .peers - .values() - .map(|peer| peer.into()) + .map(ToString::to_string) .collect(), + port: u32::from(host_config.host.listen_port), + peers: host_config.host.peers.values().map(Into::into).collect(), firewall_config: None, } } @@ -265,7 +260,7 @@ async fn main() -> Result<(), Box> { let clients = Arc::new(Mutex::new(HashMap::new())); tokio::select! { _ = grpc(config_rx, clients.clone()) => eprintln!("gRPC completed"), - _ = cli(config_tx, clients) => eprintln!("CLI completed") + () = cli(config_tx, clients) => eprintln!("CLI completed") }; Ok(()) From 0f9952ea3e75ad509b3467184a99ba0b2b7db297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 16:03:52 +0200 Subject: [PATCH 15/18] Make linter happy --- src/enterprise/firewall/mod.rs | 5 +++-- src/enterprise/firewall/nftables/mod.rs | 1 + src/enterprise/firewall/nftables/netfilter.rs | 17 +++++++++-------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index d1c9b7ce..9a61c87b 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -56,9 +56,10 @@ impl Address { } } +#[allow(dead_code)] #[derive(Debug, Copy, Clone, PartialEq)] pub(crate) enum Port { - Any, // currently it is handled with empty Vec + Any, Single(u16), Range(u16, u16), } @@ -113,6 +114,7 @@ impl fmt::Display for Port { } /// As defined in `netinet/in.h`. +#[allow(dead_code)] #[derive(Debug, Copy, Clone, PartialEq)] #[repr(u8)] pub(crate) enum Protocol { @@ -120,7 +122,6 @@ pub(crate) enum Protocol { Icmp = libc::IPPROTO_ICMP as u8, Tcp = libc::IPPROTO_TCP as u8, Udp = libc::IPPROTO_UDP as u8, - #[allow(dead_code)] IcmpV6 = libc::IPPROTO_ICMPV6 as u8, } diff --git a/src/enterprise/firewall/nftables/mod.rs b/src/enterprise/firewall/nftables/mod.rs index 2db6c0eb..d5e5574b 100644 --- a/src/enterprise/firewall/nftables/mod.rs +++ b/src/enterprise/firewall/nftables/mod.rs @@ -19,6 +19,7 @@ pub fn get_set_id() -> u32 { SET_ID_COUNTER.fetch_add(1, Ordering::Relaxed) } +#[allow(dead_code)] #[derive(Debug, Default)] enum State { #[default] diff --git a/src/enterprise/firewall/nftables/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs index d28aeb34..7174d9b0 100644 --- a/src/enterprise/firewall/nftables/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -546,7 +546,7 @@ impl FirewallRule for NatRule { // } /// Sets up the default chains for the firewall -pub(crate) fn init_firewall( +pub(super) fn init_firewall( initial_policy: Policy, defguard_fwd_chain_priority: Option, batch: &mut Batch, @@ -570,7 +570,7 @@ pub(crate) fn init_firewall( Ok(()) } -pub(crate) fn drop_table(batch: &mut Batch, ifname: &str) -> Result<(), FirewallError> { +pub(super) fn drop_table(batch: &mut Batch, ifname: &str) -> Result<(), FirewallError> { let table = Tables::Defguard(ProtoFamily::Inet).to_table(ifname); batch.add(&table, nftnl::MsgType::Add); batch.add(&table, nftnl::MsgType::Del); @@ -578,7 +578,7 @@ pub(crate) fn drop_table(batch: &mut Batch, ifname: &str) -> Result<(), Firewall Ok(()) } -pub(crate) fn drop_chain( +pub(super) fn drop_chain( chain: &Chains, batch: &mut Batch, ifname: &str, @@ -592,7 +592,7 @@ pub(crate) fn drop_chain( } /// Applies masquerade on the specified interface for the outgoing packets -pub(crate) fn set_masq( +pub(super) fn set_masq( ifname: &str, enabled: bool, batch: &mut Batch, @@ -644,7 +644,7 @@ pub(crate) fn set_masq( // Ok(()) // } -pub(crate) fn allow_established_traffic( +pub(super) fn allow_established_traffic( batch: &mut Batch, ifname: &str, ) -> Result<(), FirewallError> { @@ -667,7 +667,7 @@ pub(crate) fn allow_established_traffic( Ok(()) } -pub(crate) fn ignore_unrelated_traffic( +pub(super) fn ignore_unrelated_traffic( batch: &mut Batch, ifname: &str, ) -> Result<(), FirewallError> { @@ -691,7 +691,8 @@ pub(crate) fn ignore_unrelated_traffic( Ok(()) } -pub enum Tables { +#[allow(dead_code)] +enum Tables { Filter(ProtoFamily), Nat(ProtoFamily), Defguard(ProtoFamily), @@ -719,7 +720,7 @@ impl Tables { } } -pub enum Chains { +pub(super) enum Chains { Forward, Postrouting, } From ad97a1dfafe48ff2c9b7a9c042e7de2002af6803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 16:06:07 +0200 Subject: [PATCH 16/18] Fix PF tests --- src/enterprise/firewall/packetfilter/rule.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs index 350d44fc..25986b08 100644 --- a/src/enterprise/firewall/packetfilter/rule.rs +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -373,7 +373,7 @@ mod tests { assert_eq!(1, rules.len()); assert_eq!( rules[0].to_string(), - "pass out quick on lo0 from any to any keep state" + "pass in quick on lo0 from any to any keep state" ); // One address, one port. @@ -395,7 +395,7 @@ mod tests { assert_eq!(1, rules.len()); assert_eq!( rules[0].to_string(), - "pass out quick on lo0 from any to 192.168.1.10/24 port = 1138 keep state" + "pass in quick on lo0 from any to 192.168.1.10/24 port = 1138 keep state" ); // Two addresses, two ports. @@ -420,19 +420,19 @@ mod tests { assert_eq!(4, rules.len()); assert_eq!( rules[0].to_string(), - "pass out quick on lo0 from any to 192.168.1.10/24 port = 1138 keep state" + "pass in quick on lo0 from any to 192.168.1.10/24 port = 1138 keep state" ); assert_eq!( rules[1].to_string(), - "pass out quick on lo0 from any to 192.168.1.10/24 port = 42 keep state" + "pass in quick on lo0 from any to 192.168.1.10/24 port = 42 keep state" ); assert_eq!( rules[2].to_string(), - "pass out quick on lo0 from any to 192.168.1.20/24 port = 1138 keep state" + "pass in quick on lo0 from any to 192.168.1.20/24 port = 1138 keep state" ); assert_eq!( rules[3].to_string(), - "pass out quick on lo0 from any to 192.168.1.20/24 port = 42 keep state" + "pass in quick on lo0 from any to 192.168.1.20/24 port = 42 keep state" ); } } From 69f3c9629a1ea3a4cf887c9f1e1b590524cd6c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Fri, 30 May 2025 16:09:58 +0200 Subject: [PATCH 17/18] Make clippy happy (on Linux) --- src/enterprise/firewall/api.rs | 1 + src/enterprise/firewall/mod.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index dc3506a2..67960927 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -9,6 +9,7 @@ use super::{FirewallError, FirewallRule, Policy}; #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] const DEV_PF: &str = "/dev/pf"; +#[allow(dead_code)] pub struct FirewallApi { pub(crate) ifname: String, #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] diff --git a/src/enterprise/firewall/mod.rs b/src/enterprise/firewall/mod.rs index 9a61c87b..49174ec7 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -125,8 +125,8 @@ pub(crate) enum Protocol { IcmpV6 = libc::IPPROTO_ICMPV6 as u8, } +#[allow(dead_code)] impl Protocol { - #[cfg(target_os = "linux")] #[must_use] pub(crate) fn supports_ports(self) -> bool { matches!(self, Protocol::Tcp | Protocol::Udp) From 0d3590a2aefac71c81ad81151e30aa02662a0ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ciarcin=CC=81ski?= Date: Mon, 2 Jun 2025 09:12:06 +0200 Subject: [PATCH 18/18] cargo update --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcbb1767..477f332f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1444,9 +1444,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", "syn",