diff --git a/Cargo.lock b/Cargo.lock index ed825fc5..477f332f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,12 +67,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -263,9 +263,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "byteorder" @@ -281,9 +281,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.22" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "jobserver", "libc", @@ -304,9 +304,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -350,9 +350,9 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -445,7 +445,7 @@ dependencies = [ [[package]] name = "defguard-gateway" -version = "1.3.0" +version = "1.3.1" dependencies = [ "axum 0.8.4", "base64", @@ -454,11 +454,12 @@ dependencies = [ "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.3" +source = "git+https://github.com/DefGuard/wireguard-rs.git?rev=v0.7.3#69f1ff7064240f5f2ddbab242db0e8271733bf46" 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", @@ -851,12 +852,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", @@ -918,9 +920,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", @@ -934,9 +936,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" @@ -1031,9 +1033,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -1044,9 +1046,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", @@ -1155,13 +1157,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1288,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", @@ -1329,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" @@ -1436,9 +1444,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", "syn", @@ -1679,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" @@ -1796,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" @@ -1813,9 +1830,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1977,15 +1994,16 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 9a74686d..6f4f452d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,49 @@ [package] name = "defguard-gateway" -version = "1.3.0" +version = "1.3.1" edition = "2021" [dependencies] axum = { version = "0.8", features = ["macros"] } base64 = "0.22" clap = { version = "4.5", features = ["derive", "env"] } -defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.7.2" } +defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.7.3" } 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/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/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/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 cdb3d578..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,11 +197,11 @@ 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, } @@ -237,17 +280,28 @@ impl FirewallConfig { #[derive(Debug, Error)] pub enum FirewallError { + #[error("IP address range: {0}")] + IpAddrRange(#[from] IpAddrRangeError), + #[error("Io error: {0}")] + Io(#[from] std::io::Error), + #[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 56af9ca2..406828d0 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -26,13 +26,12 @@ use tonic::{ Request, Status, Streaming, }; -#[cfg(target_os = "linux")] -use crate::enterprise::firewall::api::FirewallManagementApi; -#[cfg(any(target_os = "linux", test))] -use crate::enterprise::firewall::FirewallRule; use crate::{ config::Config, - enterprise::firewall::{api::FirewallApi, FirewallConfig}, + enterprise::firewall::{ + api::{FirewallApi, FirewallManagementApi}, + FirewallConfig, FirewallRule, + }, error::GatewayError, execute_command, mask, proto::gateway::{ @@ -260,7 +259,6 @@ impl Gateway { } /// Checks whether the firewall config changed - #[cfg(any(target_os = "linux", test))] fn has_firewall_config_changed(&self, new_fw_config: &FirewallConfig) -> bool { if let Some(current_config) = &self.firewall_config { return current_config.default_policy != new_fw_config.default_policy @@ -271,7 +269,6 @@ impl Gateway { } /// Checks whether the firewall rules have changed. - #[cfg(any(target_os = "linux", test))] fn have_firewall_rules_changed(&self, new_rules: &[FirewallRule]) -> bool { debug!("Checking if Defguard ACL rules have changed"); if let Some(current_config) = &self.firewall_config { @@ -309,7 +306,6 @@ impl Gateway { /// should be temporary. /// /// TODO: Reduce cloning here - #[cfg(target_os = "linux")] fn process_firewall_changes( &mut self, fw_config: Option<&FirewallConfig>, @@ -320,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)?; } @@ -386,17 +382,14 @@ impl Gateway { debug!("Received configuration is identical to the current one. Skipping interface reconfiguration."); } - #[cfg(target_os = "linux")] - { - let new_firewall_configuration = - if let Some(firewall_config) = new_configuration.firewall_config { - Some(FirewallConfig::from_proto(firewall_config)?) - } else { - None - }; - - self.process_firewall_changes(new_firewall_configuration.as_ref())?; - } + let new_firewall_configuration = + if let Some(firewall_config) = new_configuration.firewall_config { + Some(FirewallConfig::from_proto(firewall_config)?) + } else { + None + }; + + self.process_firewall_changes(new_firewall_configuration.as_ref())?; Ok(()) } @@ -452,15 +445,16 @@ impl Gateway { ) -> Result>, GatewayError> { debug!("Preparing gRPC client configuration"); + let tls = ClientTlsConfig::new(); // Use CA if provided, otherwise load certificates from system. let tls = if let Some(ca) = &config.grpc_ca { let ca = read_to_string(ca).map_err(|err| { error!("Failed to read CA file: {err}"); GatewayError::InvalidCaFile })?; - ClientTlsConfig::new().ca_certificate(Certificate::from_pem(ca)) + tls.ca_certificate(Certificate::from_pem(ca)) } else { - ClientTlsConfig::new().with_native_roots() + tls.with_enabled_roots() }; let endpoint = Endpoint::from_shared(config.grpc_url.clone())? .http2_keep_alive_interval(TEN_SECS) @@ -517,28 +511,34 @@ impl Gateway { } }; } - #[cfg(target_os = "linux")] Some(update::Update::FirewallConfig(config)) => { debug!("Applying received firewall configuration: {config:?}"); - let config_str = format!("{:?}", config); + let config_str = format!("{config:?}"); match FirewallConfig::from_proto(config) { Ok(new_firewall_config) => { - debug!("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(())) => { debug!("Disabling firewall configuration"); if let Err(err) = self.process_firewall_changes(None) { error!("Failed to disable firewall configuration: {err}"); @@ -614,7 +614,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; @@ -653,13 +655,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()), @@ -789,23 +791,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, }; @@ -817,7 +827,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(), )], @@ -830,7 +840,7 @@ mod tests { }; let config_empty = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, }; @@ -849,7 +859,7 @@ mod tests { connected: Arc::new(AtomicBool::new(false)), client, stats_thread: None, - firewall_api: FirewallApi::new("test_interface"), + firewall_api: FirewallApi::new("test_interface").unwrap(), firewall_config: None, }; @@ -889,17 +899,17 @@ mod tests { #[tokio::test] async fn test_firewall_config_comparison() { let config1 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, }; let config2 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Deny, }; let config3 = FirewallConfig { - rules: vec![], + rules: Vec::new(), default_policy: Policy::Allow, }; @@ -918,7 +928,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 @@ -937,12 +947,12 @@ mod tests { 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, @@ -954,12 +964,12 @@ mod tests { let config5 = FirewallConfig { rules: vec![FirewallRule { comment: None, - destination_addrs: vec![], - destination_ports: vec![], + destination_addrs: Vec::new(), + destination_ports: Vec::new(), id: 0, verdict: Policy::Allow, - protocols: vec![], - source_addrs: vec![], + protocols: Vec::new(), + source_addrs: Vec::new(), ipv4: false, }], default_policy: Policy::Allow, 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(());