diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0989c397..16ba8185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main - dev + - 'release/**' paths-ignore: - "*.md" - "LICENSE" @@ -12,6 +13,7 @@ on: branches: - main - dev + - 'release/**' paths-ignore: - "*.md" - "LICENSE" diff --git a/.github/workflows/current.yml b/.github/workflows/current.yml index f67ce3ef..1587da18 100644 --- a/.github/workflows/current.yml +++ b/.github/workflows/current.yml @@ -4,6 +4,7 @@ on: branches: - main - dev + - 'release/**' paths-ignore: - "*.md" - "LICENSE" @@ -17,6 +18,5 @@ jobs: uses: ./.github/workflows/build-docker.yml with: tags: | - type=raw,value=current type=ref,event=branch type=sha diff --git a/Cargo.lock b/Cargo.lock index f5684811..a41891ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -43,44 +43,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[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", @@ -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.18" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -304,9 +304,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.35" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -326,9 +326,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -338,21 +338,21 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[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,20 +445,21 @@ dependencies = [ [[package]] name = "defguard-gateway" -version = "1.3.0" +version = "1.4.0" dependencies = [ - "axum 0.8.3", + "axum 0.8.4", "base64", "clap", "defguard_wireguard_rs", "env_logger", "gethostname", "ipnetwork", + "libc", "log", "mnl", "nftnl", + "nix", "prost", - "prost-build", "serde", "syslog", "thiserror 2.0.12", @@ -473,8 +474,8 @@ 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.4" +source = "git+https://github.com/DefGuard/wireguard-rs.git?rev=v0.7.4#37c1ed8aa3bb38f1fa7662a9e413d52828f18486" dependencies = [ "base64", "libc", @@ -579,9 +580,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", @@ -607,9 +608,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -671,9 +672,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,20 +682,20 @@ 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", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[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 +711,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 +724,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 +749,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -851,12 +852,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", @@ -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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[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.4", ] [[package]] @@ -1063,9 +1033,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -1076,9 +1046,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 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" @@ -1157,9 +1127,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -1178,22 +1148,22 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[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", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -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" @@ -1320,9 +1290,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -1361,6 +1331,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" @@ -1429,9 +1405,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -1442,6 +1418,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" @@ -1459,9 +1444,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", "syn", @@ -1469,9 +1454,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 +1555,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 +1595,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -1618,9 +1603,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc_version" @@ -1633,9 +1618,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 +1631,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 +1667,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", @@ -1699,9 +1687,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" @@ -1791,9 +1779,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -1816,6 +1804,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" @@ -1827,15 +1824,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[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", @@ -1861,9 +1858,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -1878,9 +1875,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 +1898,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 +1984,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,15 +1994,16 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2045,9 +2043,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 +2056,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -2070,18 +2068,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.9.0", "serde", @@ -2200,9 +2198,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ "proc-macro2", "quote", @@ -2211,9 +2209,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2247,12 +2245,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 +2265,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 +2278,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", @@ -2321,9 +2313,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -2336,9 +2328,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-sys" @@ -2424,9 +2416,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] @@ -2440,17 +2432,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 +2452,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 +2464,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 +2476,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 +2535,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 +2559,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..e2629662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,49 @@ [package] name = "defguard-gateway" -version = "1.3.0" +version = "1.4.0" 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.4" } 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"] } 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"] } +tonic = { version = "0.12", default-features = false, features = [ + "codegen", + "gzip", + "prost", + "tls-native-roots", +] } [target.'cfg(target_os = "linux")'.dependencies] nftnl = { git = "https://github.com/DefGuard/nftnl-rs.git", rev = "1a1147271f43b9d7182a114bb056a5224c35d38f" } mnl = "0.2" +[target.'cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))'.dependencies] +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] -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/deny.toml b/deny.toml index 9305cfe7..05ea3fa6 100644 --- a/deny.toml +++ b/deny.toml @@ -88,13 +88,18 @@ ignore = [ allow = [ "MIT", "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "MPL-2.0", "BSD-3-Clause", "Unicode-3.0", + "Unicode-DFS-2016", # unicode-ident "Zlib", "ISC", "BSL-1.0", "0BSD", - "AGPL-3.0", + "CC0-1.0", + "OpenSSL", + "CDLA-Permissive-2.0", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the @@ -104,9 +109,7 @@ confidence-threshold = 0.8 # Allow 1 or more licenses on a per-crate basis, so that particular licenses # aren't accepted for every possible crate as with the normal allow list exceptions = [ - # Each entry is the crate and version constraint, and its specific allow - # list - #{ allow = ["Zlib"], crate = "adler32" }, + { allow = ["AGPL-3.0"], crate = "defguard-gateway" }, ] # Some crates don't have (easily) machine readable licensing information, diff --git a/examples/server.rs b/examples/server.rs index c7250581..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, } } @@ -107,7 +102,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(())) } @@ -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(()) diff --git a/flake.lock b/flake.lock index abd9d138..19484013 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1743315132, - "narHash": "sha256-6hl6L/tRnwubHcA4pfUUtk542wn2Om+D4UnDhlDW9BE=", + "lastModified": 1746904237, + "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "52faf482a3889b7619003c0daec593a1912fddc1", + "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1743475035, - "narHash": "sha256-uLjVsb4Rxnp1zmFdPCDmdODd4RY6ETOeRj0IkC0ij/4=", + "lastModified": 1747190175, + "narHash": "sha256-s33mQ2s5L/2nyllhRTywgECNZyCqyF4MJeM3vG/GaRo=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "bee11c51c2cda3ac57c9e0149d94b86cc1b00d13", + "rev": "58160be7abad81f6f8cb53120d5b88c16e01c06d", "type": "github" }, "original": { diff --git a/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc b/opnsense/src/etc/inc/plugins.inc.d/defguardgateway.inc index 1c1b8a6f..e15d494d 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', 1, 'head'); + // $fw->registerAnchor('defguard/*', 'rdr', 1, 'head'); + $fw->registerAnchor('defguard/*', 'fw', 1, 'head', true); +} diff --git a/proto b/proto index 289f58ef..d72ced89 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 289f58ef42617bfbdd75d98a62ac9aec2ab8d1d6 +Subproject commit d72ced898411c4b8144bb13a9ad48f65e2f6a1ec diff --git a/src/config.rs b/src/config.rs index 619a58e1..c8af948c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -99,6 +99,12 @@ pub struct Config { #[arg(long, env = "DEFGUARD_FW_PRIORITY")] #[serde(default)] pub fw_priority: Option, + + /// Whether all firewall management should be disabled + /// Meant to be used as a workaround for incompatible hardware + #[arg(long, env = "DEFGUARD_DISABLE_FW_MGMT")] + #[serde(default)] + pub disable_firewall_management: bool, } impl Default for Config { @@ -123,6 +129,7 @@ impl Default for Config { health_port: None, masquerade: false, fw_priority: None, + disable_firewall_management: false, } } } diff --git a/src/enterprise/firewall/api.rs b/src/enterprise/firewall/api.rs index 1950ea7e..67960927 100644 --- a/src/enterprise/firewall/api.rs +++ b/src/enterprise/firewall/api.rs @@ -1,39 +1,56 @@ +#[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] +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", target_os = "netbsd"))] +const DEV_PF: &str = "/dev/pf"; + +#[allow(dead_code)] pub struct FirewallApi { - pub ifname: String, + pub(crate) ifname: String, + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] + pub(crate) file: File, + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] + pub(crate) default_policy: Policy, #[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", target_os = "netbsd"))] + file: OpenOptions::new().read(true).write(true).open(DEV_PF)?, + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] + default_policy: Policy::Deny, #[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>; - fn add_rule(&mut self, rule: FirewallRule) -> Result<(), FirewallError>; + + /// Add fireall `rules`. fn add_rules(&mut self, rules: Vec) -> Result<(), FirewallError>; - 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>; - fn rollback(&mut self); } diff --git a/src/enterprise/firewall/dummy/mod.rs b/src/enterprise/firewall/dummy/mod.rs index e09b3919..9129f91f 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(()) @@ -17,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(()) } @@ -29,25 +24,11 @@ impl FirewallManagementApi for FirewallApi { Ok(()) } - fn add_rule(&mut self, _rule: FirewallRule) -> Result<(), FirewallError> { - Ok(()) - } - fn begin(&mut self) -> Result<(), FirewallError> { Ok(()) } - fn rollback(&mut self) {} - fn commit(&mut self) -> Result<(), FirewallError> { 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..33eab39d --- /dev/null +++ b/src/enterprise/firewall/iprange.rs @@ -0,0 +1,129 @@ +//! 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::RangeInclusive, +}; + +#[derive(Clone, Debug, PartialEq)] +pub enum IpAddrRange { + V4(RangeInclusive), + V6(RangeInclusive), +} + +#[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"), + } + } +} + +#[allow(dead_code)] +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(start..=end)), + (IpAddr::V6(start), IpAddr::V6(end)) => Ok(Self::V6(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 = start..=end; + + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + assert!(range.contains(&addr)); + + let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)); + assert!(!range.contains(&addr)); + + // 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_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 addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)); + assert!(range.contains(&addr)); + + 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/mod.rs b/src/enterprise/firewall/mod.rs index f68c6b85..49174ec7 100644 --- a/src/enterprise/firewall/mod.rs +++ b/src/enterprise/firewall/mod.rs @@ -1,29 +1,31 @@ -use std::{net::IpAddr, str::FromStr}; +pub mod api; +#[cfg(test)] +mod dummy; +mod iprange; +#[cfg(all(not(test), target_os = "linux"))] +mod nftables; +#[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] +mod packetfilter; + +use std::{fmt, 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,10 @@ impl Address { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Port { +#[allow(dead_code)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub(crate) enum Port { + Any, Single(u16), Range(u16, u16), } @@ -99,26 +103,65 @@ impl Port { } } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Protocol(pub u8); +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}}}"), + } + } +} -// 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`. +#[allow(dead_code)] +#[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, +} +#[allow(dead_code)] impl Protocol { #[must_use] - pub fn supports_ports(&self) -> bool { - PORT_PROTOCOLS.contains(self) + pub(crate) fn supports_ports(self) -> bool { + matches!(self, Protocol::Tcp | Protocol::Udp) + } + + 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), + // TODO: IcmpV6 + proto::enterprise::firewall::Protocol::Invalid => { + Err(FirewallError::UnsupportedProtocol(proto as u8)) + } + } + } +} + +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, Eq)] -pub enum Policy { +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub(crate) enum Policy { #[default] Allow, Deny, @@ -144,8 +187,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 +197,13 @@ 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, } impl FirewallConfig { @@ -171,12 +212,13 @@ impl FirewallConfig { ) -> Result { debug!("Parsing following received firewall proto configuration: {config:?}"); let mut rules = Vec::new(); - let v4 = config.ip_version == proto::enterprise::firewall::IpVersion::Ipv4 as i32; let default_policy = 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!( + "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:?}"); @@ -212,6 +254,7 @@ impl FirewallConfig { FirewallError::TypeConversionError(format!("Invalid rule verdict: {err:?}")) })?); + let ipv4 = rule.ip_version == proto::enterprise::firewall::IpVersion::Ipv4 as i32; let firewall_rule = FirewallRule { id: rule.id, source_addrs, @@ -219,7 +262,7 @@ impl FirewallConfig { destination_ports, protocols, verdict, - ipv4: v4, + ipv4, comment: rule.comment, }; @@ -231,24 +274,34 @@ impl FirewallConfig { Ok(Self { rules, default_policy, - ipv4: v4, }) } } #[derive(Debug, Error)] pub enum FirewallError { + #[error("IP address range: {0}")] + IpAddrRange(#[from] IpAddrRangeError), + #[error("Io error: {0}")] + Io(#[from] std::io::Error), + #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] + #[error("Errno:{0}")] + Errno(#[from] nix::errno::Errno), #[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 70% rename from src/enterprise/firewall/linux/mod.rs rename to src/enterprise/firewall/nftables/mod.rs index 3a43d046..d5e5574b 100644 --- a/src/enterprise/firewall/linux/mod.rs +++ b/src/enterprise/firewall/nftables/mod.rs @@ -2,18 +2,16 @@ 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, + init_firewall, send_batch, set_masq, }; 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,23 +19,9 @@ 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)) - } - } - } -} - +#[allow(dead_code)] #[derive(Debug, Default)] -pub enum State { +enum State { #[default] Established, Invalid, @@ -46,100 +30,30 @@ 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 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: Option, - 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 @@ -147,9 +61,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, @@ -163,9 +83,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 { @@ -182,7 +105,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, @@ -199,10 +125,11 @@ 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 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, @@ -227,6 +154,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() { @@ -237,16 +236,13 @@ 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(), + )) } } - 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/linux/netfilter.rs b/src/enterprise/firewall/nftables/netfilter.rs similarity index 88% rename from src/enterprise/firewall/linux/netfilter.rs rename to src/enterprise/firewall/nftables/netfilter.rs index e0cff456..7174d9b0 100644 --- a/src/enterprise/firewall/linux/netfilter.rs +++ b/src/enterprise/firewall/nftables/netfilter.rs @@ -1,14 +1,13 @@ #[cfg(test)] use std::str::FromStr; use std::{ - ffi::CString, + ffi::{CStr, CString}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, }; 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 +16,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-{IFNAME}"; +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 +53,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 +75,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 +89,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 +101,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(ipv4_range) => { + add_to_set(set, ipv4_range.start(), Some(ipv4_range.end()))?; + } + IpAddrRange::V6(ipv6_range) => { + add_to_set(set, ipv6_range.start(), Some(ipv6_range.end()))?; + } + }, } Ok(()) @@ -137,6 +122,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 +155,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 +312,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 +342,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 +357,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 +370,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 +408,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 { @@ -436,13 +423,13 @@ 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"); } let matches = matches.join(" AND "); - debug!("Created nftables rule with matches: {:?}", matches); + debug!("Created nftables rule with matches: {matches:?}"); Ok(rule) } @@ -559,8 +546,8 @@ impl FirewallRule for NatRule { // } /// Sets up the default chains for the firewall -pub(crate) fn init_firewall( - initial_policy: Option, +pub(super) fn init_firewall( + initial_policy: Policy, defguard_fwd_chain_priority: Option, batch: &mut Batch, ifname: &str, @@ -576,14 +563,14 @@ 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); 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); @@ -591,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, @@ -605,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, @@ -638,26 +625,26 @@ 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( +pub(super) fn allow_established_traffic( batch: &mut Batch, ifname: &str, ) -> Result<(), FirewallError> { @@ -680,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> { @@ -704,7 +691,8 @@ pub(crate) fn ignore_unrelated_traffic( Ok(()) } -pub enum Tables { +#[allow(dead_code)] +enum Tables { Filter(ProtoFamily), Nat(ProtoFamily), Defguard(ProtoFamily), @@ -724,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, ), @@ -732,7 +720,7 @@ impl Tables { } } -pub enum Chains { +pub(super) enum Chains { Forward, Postrouting, } @@ -754,7 +742,7 @@ impl Chains { } } -pub(crate) fn apply_filter_rules( +pub(super) fn apply_filter_rules( rules: Vec, batch: &mut Batch, ifname: &str, @@ -828,16 +816,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)) } } } @@ -850,13 +838,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/api.rs b/src/enterprise/firewall/packetfilter/api.rs new file mode 100644 index 00000000..baad67cd --- /dev/null +++ b/src/enterprise/firewall/packetfilter/api.rs @@ -0,0 +1,91 @@ +use std::os::fd::AsRawFd; + +use super::{ + calls::{pf_begin, pf_commit, pf_rollback, IocTrans, IocTransElement}, + rule::RuleSet, + 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 = self.get_pool_ticket(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 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(()) + } +} diff --git a/src/enterprise/firewall/packetfilter/calls.rs b/src/enterprise/firewall/packetfilter/calls.rs new file mode 100644 index 00000000..e81b7312 --- /dev/null +++ b/src/enterprise/firewall/packetfilter/calls.rs @@ -0,0 +1,920 @@ +//! 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}, + fmt, + mem::{size_of, zeroed, MaybeUninit}, + ptr, +}; + +use ipnetwork::IpNetwork; +use libc::{pid_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]; + +/// Equivalent to `struct pf_addr_wrap_addr_mask`. +#[derive(Clone, Copy, 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(), + }, + } + } +} + +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. +#[repr(C)] +struct AddrWrap { + v: VTarget, + // Unused in this crate. + p: u64, + // Determines type of field `v`. + r#type: AddrType, + // See PFI_AFLAG + iflags: u8, +} + +#[allow(dead_code)] +#[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, + // Values below differ on macOS and FreeBSD. + // PF_ADDR_RTLABEL = 4, + // RtLabel, + // // PF_ADDR_URPFFAILED = 5, + // UrpfFailed, + // // PF_ADDR_RANGE = 6, + // Range, +} + +impl AddrWrap { + #[must_use] + fn with_network(ip_network: IpNetwork) -> Self { + Self { + v: VTarget { + a: ip_network.into(), + }, + p: 0, + r#type: AddrType::AddrMask, + iflags: 0, + } + } + + #[allow(dead_code)] + #[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`. +#[derive(Debug)] +#[repr(C)] +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(any(target_os = "macos", target_os = "netbsd"))] + neg: c_uchar, + #[cfg(target_os = "netbsd")] + op: PortOp, +} + +impl RuleAddr { + #[must_use] + pub(super) fn new(ip_network: IpNetwork, port: Port) -> Self { + let addr = AddrWrap::with_network(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(any(target_os = "macos", target_os = "netbsd"))] + neg: 0, + } + } +} + +#[derive(Debug)] +#[repr(C)] +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`. +#[derive(Debug)] +#[repr(C)] +pub struct PoolAddr { + addr: AddrWrap, + entries: TailQueueEntry, + ifname: [u8; IFNAMSIZ], + kif: usize, // *mut c_void, +} + +impl PoolAddr { + #[allow(dead_code)] + #[must_use] + pub fn with_network(ip_network: IpNetwork) -> Self { + Self { + addr: AddrWrap::with_network(ip_network), + entries: unsafe { zeroed::>() }, + ifname: [0; IFNAMSIZ], + kif: 0, + } + } + + #[allow(dead_code)] + #[must_use] + pub fn with_interface(ifname: &str) -> Self { + Self { + addr: AddrWrap::with_interface(ifname), + entries: unsafe { zeroed::>() }, + ifname: [0; IFNAMSIZ], + kif: 0, + } + } +} + +#[allow(dead_code)] +#[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: TailQueue, + cur: *mut PoolAddr, + key: PoolHashKey, + counter: Addr, + tblidx: c_int, + pub(super) proxy_port: [c_ushort; 2], + #[cfg(any(target_os = "macos", target_os = "netbsd"))] + port_op: PortOp, + pub(super) opts: PoolOpts, + #[cfg(target_os = "macos")] + af: AddressFamily, +} + +#[allow(dead_code)] +#[derive(Debug)] +#[repr(u8)] +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)) +} + +#[allow(dead_code)] +impl Pool { + #[must_use] + pub(super) 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] = from_port; + (*self_ptr).proxy_port[1] = to_port; + } + + 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; + } +} + +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)] +struct pf_anchor_node { + rbe_left: *mut pf_anchor, + rbe_right: *mut pf_anchor, + rbe_parent: *mut pf_anchor, +} + +#[repr(C)] +struct pf_ruleset_rule { + ptr: *mut TailQueue, + ptr_array: *mut *mut Rule, + rcount: c_uint, + rsize: c_uint, + ticket: c_uint, + open: c_int, +} + +#[repr(C)] +struct pf_ruleset_rules { + queues: [TailQueue; 2], + active: pf_ruleset_rule, + inactive: pf_ruleset_rule, +} + +#[repr(C)] +struct pf_ruleset { + rules: [pf_ruleset_rules; 6], + anchor: *mut pf_anchor, + tticket: c_uint, + tables: c_int, + topen: c_int, +} + +#[repr(C)] +struct pf_anchor { + entry_global: pf_anchor_node, + 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)] +struct pf_rule_conn_rate { + limit: c_uint, + seconds: c_uint, +} + +#[derive(Debug)] +#[repr(C)] +struct pf_rule_id { + uid: [uid_t; 2], + op: c_uchar, + //_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(super) struct Rule { + src: RuleAddr, + dst: RuleAddr, + + 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], + + entries: TailQueueEntry, + pub(super) rpool: Pool, + + evaluations: c_long, + packets: [c_ulong; 2], + bytes: [c_ulong; 2], + + #[cfg(target_os = "macos")] + ticket: c_ulong, + #[cfg(target_os = "macos")] + owner: [c_char; 64], + #[cfg(target_os = "macos")] + priority: c_int, + + kif: *mut c_void, // struct pfi_kif, kernel only + anchor: *mut pf_anchor, + overload_tbl: *mut c_void, // struct pfr_ktable, kernel only + + os_fingerprint: c_uint, + + rtableid: c_int, + #[cfg(any(target_os = "freebsd", target_os = "netbsd"))] + timeout: [c_uint; 20], + #[cfg(target_os = "macos")] + timeout: [c_uint; 26], + #[cfg(any(target_os = "macos", target_os = "netbsd"))] + states: c_uint, + max_states: c_uint, + #[cfg(any(target_os = "macos", target_os = "netbsd"))] + 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")] + states_cur: u64, + #[cfg(target_os = "freebsd")] + states_tot: u64, + #[cfg(target_os = "freebsd")] + src_nodes: u64, + + return_icmp: c_ushort, + return_icmp6: c_ushort, + max_mss: c_ushort, + tag: c_ushort, + match_tag: c_ushort, + #[cfg(target_os = "freebsd")] + 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, + 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")] + set_tos: c_uchar, + anchor_relative: c_uchar, + anchor_wildcard: c_uchar, + flush: c_uchar, + #[cfg(target_os = "freebsd")] + prio: c_uchar, + #[cfg(target_os = "freebsd")] + set_prio: [c_uchar; 2], + + #[cfg(target_os = "freebsd")] + divert: (Addr, u16), + + #[cfg(target_os = "freebsd")] + u_states_cur: u64, + #[cfg(target_os = "freebsd")] + u_states_tot: u64, + #[cfg(target_os = "freebsd")] + u_src_nodes: u64, + + #[cfg(target_os = "macos")] + proto_variant: c_uchar, + #[cfg(target_os = "macos")] + extfilter: c_uchar, + #[cfg(target_os = "macos")] + extmap: c_uchar, + #[cfg(target_os = "macos")] + dnpipe: c_uint, + #[cfg(target_os = "macos")] + dntype: c_uint, +} + +impl Rule { + pub(super) fn from_pf_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(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]); + } + + // Don't use routing tables. + #[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.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; + + (*self_ptr).rpool.list.init(); + + uninit.assume_init() + } + } +} + +/// Equivalent to PF_CHANGE_... enum. +#[allow(dead_code)] +#[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_... +#[allow(dead_code)] +#[repr(u32)] +pub(crate) enum RuleFlag { + Drop = 0, + ReturnRST = 1, + Fragment = 2, + ReturnICMP = 4, + Return = 8, + NoSync = 16, + SrcTrack = 32, + RuleSrcTrack = 64, + // ... +} + +pub(crate) const MAXPATHLEN: usize = libc::PATH_MAX as usize; + +/// Equivalent to `struct pfioc_rule`. +#[repr(C)] +pub(super) 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(super) 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(super) struct IocPoolAddr { + action: Change, + 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], + addr: PoolAddr, +} + +impl IocPoolAddr { + #[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() } + } + + #[allow(dead_code)] + #[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`. +#[repr(C)] +pub(super) struct IocTransElement { + rs_num: RuleSet, + anchor: [c_uchar; MAXPATHLEN], + pub(super) ticket: c_uint, +} + +impl IocTransElement { + #[must_use] + pub(super) fn new(ruleset: RuleSet, 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).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(super) 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(super) fn new(elements: &mut [IocTransElement]) -> Self { + Self { + size: elements.len() as i32, + esize: size_of::() as i32, + array: elements.as_mut_ptr(), + } + } +} + +// 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 +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 +// 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 +// 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); + +// 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); + +// DIOCGETRULESET +// 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); + +// DIOCXROLLBACK +ioctl_readwrite!(pf_rollback, b'D', 83, IocTrans); + +#[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); + #[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] + fn check_addr_mask() { + let ipnetv4 = IpNetwork::V4(Ipv4Network::new(Ipv4Addr::LOCALHOST, 8).unwrap()); + + let addr_mask = AddrMask::from(ipnetv4); + assert_eq!( + addr_mask.addr, + [127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + assert_eq!( + 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 = AddrMask::from(ipv6); + assert_eq!( + addr_wrap.addr, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + ); + assert_eq!( + 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 new file mode 100644 index 00000000..81cccdce --- /dev/null +++ b/src/enterprise/firewall/packetfilter/mod.rs @@ -0,0 +1,96 @@ +//! 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; + +use std::os::fd::{AsRawFd, RawFd}; + +use calls::{pf_begin_addrs, IocPoolAddr}; +use rule::PacketFilterRule; + +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/"; + +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() + } + + 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, + pool_ticket: u32, + anchor: &str, + ) -> Result<(), FirewallError> { + let rule = PacketFilterRule::for_policy(self.default_policy, &self.ifname); + let mut ioc = IocRule::with_rule(anchor, Rule::from_pf_rule(&rule)); + 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, + rule: &mut FirewallRule, + ticket: u32, + pool_ticket: u32, + anchor: &str, + ) -> Result<(), FirewallError> { + debug!("add_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)); + 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(()) + } +} + +#[cfg(not(test))] +mod api; diff --git a/src/enterprise/firewall/packetfilter/rule.rs b/src/enterprise/firewall/packetfilter/rule.rs new file mode 100644 index 00000000..25986b08 --- /dev/null +++ b/src/enterprise/firewall/packetfilter/rule.rs @@ -0,0 +1,438 @@ +use std::fmt; + +use ipnetwork::IpNetwork; +use libc::{AF_INET, AF_INET6, AF_UNSPEC}; + +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 { + /// 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, +} + +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}") + } +} + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub(super) enum AddressFamily { + Unspec = AF_UNSPEC as u8, + Inet = AF_INET as u8, + Inet6 = AF_INET6 as u8, +} + +/// Packet filter rule direction. +#[allow(dead_code)] +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum Direction { + /// PF_INOUT = 0 + InOut, + /// PF_IN = 1 + In, + /// PF_OUT = 2 + 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; +// #[cfg(target_os = "freebsd")] +// const PF_LOG_FORCE: u8 = 0x08; +// #[cfg(target_os = "freebsd")] +// const PF_LOG_MATCHES: u8 = 0x10; + +/// Equivalent to `PF_RULESET_...`. +#[allow(dead_code)] +#[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_...`. +#[allow(dead_code)] +#[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, +} + +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. +#[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)] +pub(super) struct PacketFilterRule { + /// Source address; `Option::None` means "any". + pub(super) from: Option, + /// Source port; 0 means "any". + pub(super) from_port: Port, + /// Destination address; `Option::None` means "any". + pub(super) to: Option, + /// Destination port; 0 means "any". + pub(super) to_port: Port, + pub(super) action: Action, + pub(super) direction: Direction, + pub(super) quick: bool, + /// See `PF_LOG`. + pub(super) log: u8, + pub(super) 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 { + /// Default rule for policy. + #[must_use] + pub(super) fn for_policy(policy: Policy, ifname: &str) -> Self { + let (action, state) = match policy { + Policy::Allow => (Action::Pass, State::Normal), + Policy::Deny => (Action::Drop, State::None), + }; + Self { + from: None, + from_port: Port::Any, + to: None, + to_port: Port::Any, + action, + direction: Direction::In, + quick: false, + log: PF_LOG, + state, + interface: Some(ifname.to_owned()), + proto: Protocol::Any, + tcp_flags: TH_SYN, + tcp_flags_set: TH_SYN | TH_ACK, + label: None, + } + } + + /// Determine address family. + pub(super) fn address_family(&self) -> AddressFamily { + match self.to { + 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, + } + } + + /// 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, state) = match fr.verdict { + Policy::Allow => (Action::Pass, State::Normal), + Policy::Deny => (Action::Drop, State::None), + }; + + let mut from_addrs = Vec::new(); + if fr.source_addrs.is_empty() { + from_addrs.push(None); + } else { + for src in &fr.source_addrs { + match src { + Address::Network(net) => from_addrs.push(Some(*net)), + Address::Range(range) => { + for addr in range.clone() { + from_addrs.push(Some(IpNetwork::from(addr))); + } + } + } + } + } + + let mut to_addrs = Vec::new(); + if fr.destination_addrs.is_empty() { + to_addrs.push(None); + } else { + for src in &fr.destination_addrs { + match src { + Address::Network(net) => to_addrs.push(Some(*net)), + Address::Range(range) => { + for addr in range.clone() { + to_addrs.push(Some(IpNetwork::from(addr))); + } + } + } + } + } + + if fr.destination_ports.is_empty() { + fr.destination_ports.push(Port::Any); + } + + if fr.protocols.is_empty() { + fr.protocols.push(Protocol::Any); + } + + 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, + from_port: Port::Any, + to: *to, + to_port: *to_port, + action, + direction: Direction::In, + // Enable quick to match NFTables behaviour. + quick: true, + log: PF_LOG, + state, + interface: Some(ifname.to_owned()), + proto: *proto, + // For stateful connections, the default is flags S/SA. + tcp_flags: TH_SYN, + tcp_flags_set: TH_SYN | TH_ACK, + label: fr.comment.clone(), + }; + rules.push(rule); + } + } + } + } + + rules + } +} + +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")?; + } + // TODO: tcp_flags/tcp_flags_set + write!(f, " {} {}", self.to_port, self.state)?; + if let Some(label) = &self.label { + write!(f, " label \"{label}\"")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::net::{IpAddr, Ipv4Addr}; + + use super::*; + + #[test] + fn unroll_firewall_rule() { + // Empty rule + let mut fr = FirewallRule { + comment: None, + destination_addrs: Vec::new(), + destination_ports: Vec::new(), + id: 0, + verdict: Policy::Allow, + protocols: Vec::new(), + source_addrs: Vec::new(), + ipv4: true, + }; + + let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); + assert_eq!(1, rules.len()); + assert_eq!( + rules[0].to_string(), + "pass in quick on lo0 from any to any keep state" + ); + + // One address, one port. + let addr1 = Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), 24).unwrap(), + ); + let mut fr = FirewallRule { + comment: None, + destination_addrs: vec![addr1], + destination_ports: vec![Port::Single(1138)], + id: 0, + verdict: Policy::Allow, + protocols: Vec::new(), + source_addrs: Vec::new(), + ipv4: true, + }; + + let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); + 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 keep state" + ); + + // Two addresses, two ports. + let addr1 = Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), 24).unwrap(), + ); + let addr2 = Address::Network( + IpNetwork::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 20)), 24).unwrap(), + ); + let mut fr = FirewallRule { + comment: None, + destination_addrs: vec![addr1, addr2], + destination_ports: vec![Port::Single(1138), Port::Single(42)], + id: 0, + verdict: Policy::Allow, + protocols: Vec::new(), + source_addrs: Vec::new(), + ipv4: true, + }; + + let rules = PacketFilterRule::from_firewall_rule("lo0", &mut fr); + 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 keep state" + ); + assert_eq!( + rules[1].to_string(), + "pass in 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 keep state" + ); + assert_eq!( + rules[3].to_string(), + "pass in quick on lo0 from any to 192.168.1.20/24 port = 42 keep state" + ); + } +} diff --git a/src/gateway.rs b/src/gateway.rs index 6e47daa5..039e3e9e 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,20 +259,17 @@ 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 - || current_config.ipv4 != new_fw_config.ipv4 - || self.has_firewall_rules_changed(&new_fw_config.rules); + || self.have_firewall_rules_changed(&new_fw_config.rules); } true } /// Checks whether the firewall rules have changed. - #[cfg(any(target_os = "linux", test))] - fn has_firewall_rules_changed(&self, new_rules: &[FirewallRule]) -> bool { + 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 { let current_rules = ¤t_config.rules; @@ -310,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>, @@ -321,7 +316,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)?; } @@ -387,8 +382,8 @@ impl Gateway { debug!("Received configuration is identical to the current one. Skipping interface reconfiguration."); } - #[cfg(target_os = "linux")] - { + // process received firewall config unless firewall management is disabled + if !self.config.disable_firewall_management { let new_firewall_configuration = if let Some(firewall_config) = new_configuration.firewall_config { Some(FirewallConfig::from_proto(firewall_config)?) @@ -397,6 +392,8 @@ impl Gateway { }; self.process_firewall_changes(new_firewall_configuration.as_ref())?; + } else { + debug!("Firewall management is disabled. Skipping updating firewall configuration"); } Ok(()) @@ -453,15 +450,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) @@ -518,28 +516,44 @@ impl Gateway { } }; } - #[cfg(target_os = "linux")] Some(update::Update::FirewallConfig(config)) => { + if self.config.disable_firewall_management { + debug!("Received firewall config update, but firewall management is disabled. Skipping processing this update: {config:?}"); + continue; + } + 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!("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(_)) => { + Some(update::Update::DisableFirewall(())) => { + if self.config.disable_firewall_management { + debug!("Received firewall disable request, but firewall management is disabled. Skipping processing this update"); + continue; + } + debug!("Disabling firewall configuration"); if let Err(err) = self.process_firewall_changes(None) { error!("Failed to disable firewall configuration: {err}"); @@ -581,7 +595,7 @@ impl Gateway { ); } else { #[cfg(target_os = "linux")] - if self.config.masquerade { + if !self.config.disable_firewall_management && self.config.masquerade { self.firewall_api.begin()?; self.firewall_api.set_masquerade_status(true)?; self.firewall_api.commit()?; @@ -615,7 +629,9 @@ impl Gateway { #[cfg(test)] mod tests { - #[cfg(not(target_os = "macos"))] + use std::net::Ipv4Addr; + + #[cfg(not(any(target_os = "macos", target_os = "netbsd")))] use defguard_wireguard_rs::Kernel; #[cfg(target_os = "macos")] use defguard_wireguard_rs::Userspace; @@ -654,13 +670,13 @@ 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(); 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()), @@ -790,23 +806,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, }; @@ -818,7 +842,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(), )], @@ -828,13 +852,11 @@ mod tests { let config1 = FirewallConfig { rules: vec![rule1.clone(), rule2.clone()], default_policy: Policy::Allow, - ipv4: true, }; let config_empty = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, - ipv4: true, }; #[cfg(target_os = "macos")] @@ -852,67 +874,58 @@ 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 firewall config, new rules are empty gateway.firewall_config = None; - assert!(gateway.has_firewall_rules_changed(&[])); + assert!(gateway.have_firewall_rules_changed(&[])); // Gateway has no firewall config, but new rules exist gateway.firewall_config = None; - assert!(gateway.has_firewall_rules_changed(&[rule1.clone()])); + assert!(gateway.have_firewall_rules_changed(&[rule1.clone()])); // Gateway has firewall config, with empty rules list gateway.firewall_config = Some(config1.clone()); - assert!(gateway.has_firewall_rules_changed(&[])); + assert!(gateway.have_firewall_rules_changed(&[])); // Gateway has firewall config, new rules have different length gateway.firewall_config = Some(config1.clone()); - assert!(gateway.has_firewall_rules_changed(&[rule1.clone()])); + assert!(gateway.have_firewall_rules_changed(&[rule1.clone()])); // Gateway has firewall config, new rules have different content gateway.firewall_config = Some(config1.clone()); - assert!(gateway.has_firewall_rules_changed(&[rule1.clone(), rule3.clone()])); + assert!(gateway.have_firewall_rules_changed(&[rule1.clone(), rule3.clone()])); // Gateway has firewall config, new rules are identical gateway.firewall_config = Some(config1.clone()); - assert!(!gateway.has_firewall_rules_changed(&[rule1.clone(), rule2.clone()])); + assert!(!gateway.have_firewall_rules_changed(&[rule1.clone(), rule2.clone()])); // Gateway has empty firewall config, new rules exist gateway.firewall_config = Some(config_empty.clone()); - assert!(gateway.has_firewall_rules_changed(&[rule1.clone()])); + assert!(gateway.have_firewall_rules_changed(&[rule1.clone()])); // Both configs are empty gateway.firewall_config = Some(config_empty); - assert!(!gateway.has_firewall_rules_changed(&[])); + assert!(!gateway.have_firewall_rules_changed(&[])); } #[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![], - default_policy: Policy::Allow, - ipv4: true, }; #[cfg(target_os = "macos")] @@ -930,7 +943,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 @@ -941,30 +954,42 @@ mod tests { gateway.firewall_config = Some(config1.clone()); assert!(gateway.has_firewall_config_changed(&config2)); - // Gateway has config, new config has different v4 value - gateway.firewall_config = Some(config1.clone()); - assert!(gateway.has_firewall_config_changed(&config3)); - // Gateway has config, new config is identical gateway.firewall_config = Some(config1.clone()); - assert!(!gateway.has_firewall_config_changed(&config4)); + assert!(!gateway.has_firewall_config_changed(&config3)); // Rules are not being ignored - let config5 = FirewallConfig { + let config4 = 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, - ipv4: true, }; gateway.firewall_config = Some(config1); + assert!(gateway.has_firewall_config_changed(&config4)); + + // Rule IP versions are not being ignored + let config5 = FirewallConfig { + rules: vec![FirewallRule { + comment: None, + destination_addrs: Vec::new(), + destination_ports: Vec::new(), + id: 0, + verdict: Policy::Allow, + protocols: Vec::new(), + source_addrs: Vec::new(), + ipv4: false, + }], + default_policy: Policy::Allow, + }; + gateway.firewall_config = Some(config4); assert!(gateway.has_firewall_config_changed(&config5)); } } diff --git a/src/main.rs b/src/main.rs index 3a129cce..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}; @@ -39,18 +39,18 @@ 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)?; 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(());