diff options
| -rw-r--r-- | ci/ios/test-router/flake.lock | 8 | ||||
| -rw-r--r-- | ci/ios/test-router/flake.nix | 2 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/Cargo.lock | 10 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/Cargo.toml | 2 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/block_list/mod.rs | 2 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/block_list/rule.rs | 140 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/capture/mod.rs | 9 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/capture/parse.rs | 4 | ||||
| -rw-r--r-- | ci/ios/test-router/raas/src/web/routes.rs | 99 | ||||
| -rw-r--r-- | ci/ios/test-router/router-config.nix | 2 |
10 files changed, 189 insertions, 89 deletions
diff --git a/ci/ios/test-router/flake.lock b/ci/ios/test-router/flake.lock index 064ee7a736..6a449efb7e 100644 --- a/ci/ios/test-router/flake.lock +++ b/ci/ios/test-router/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1720535198, - "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=", + "lastModified": 1742937945, + "narHash": "sha256-lWc+79eZRyvHp/SqMhHTMzZVhpxkRvthsP1Qx6UCq0E=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5", + "rev": "d02d88f8de5b882ccdde0465d8fa2db3aa1169f7", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-23.11", + "ref": "nixos-24.11", "repo": "nixpkgs", "type": "github" } diff --git a/ci/ios/test-router/flake.nix b/ci/ios/test-router/flake.nix index 7d7593907c..0650830934 100644 --- a/ci/ios/test-router/flake.nix +++ b/ci/ios/test-router/flake.nix @@ -1,7 +1,7 @@ { description = "Config for our testing router"; - inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; }; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; }; outputs = { self, nixpkgs }: { nixosConfigurations.app-team-ios-lab = nixpkgs.lib.nixosSystem { diff --git a/ci/ios/test-router/raas/Cargo.lock b/ci/ios/test-router/raas/Cargo.lock index ec9f29813b..04cc80d840 100644 --- a/ci/ios/test-router/raas/Cargo.lock +++ b/ci/ios/test-router/raas/Cargo.lock @@ -422,6 +422,15 @@ dependencies = [ ] [[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" +dependencies = [ + "serde", +] + +[[package]] name = "is-terminal" version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -752,6 +761,7 @@ dependencies = [ "axum", "env_logger", "futures", + "ipnetwork", "log", "mnl", "nftnl", diff --git a/ci/ios/test-router/raas/Cargo.toml b/ci/ios/test-router/raas/Cargo.toml index 5bc0eacf6e..38b59f6480 100644 --- a/ci/ios/test-router/raas/Cargo.toml +++ b/ci/ios/test-router/raas/Cargo.toml @@ -29,4 +29,4 @@ serde = { version = "1.0.138", features = ["derive"] } serde_json = "1.0" uuid = { version = "1", features = [ "serde" ] } anyhow = "1" - +ipnetwork = { version = "0.21", features = [ "serde" ]} diff --git a/ci/ios/test-router/raas/src/block_list/mod.rs b/ci/ios/test-router/raas/src/block_list/mod.rs index 8aab004126..82eece7bb1 100644 --- a/ci/ios/test-router/raas/src/block_list/mod.rs +++ b/ci/ios/test-router/raas/src/block_list/mod.rs @@ -6,7 +6,7 @@ static TABLE_NAME: Lazy<CString> = Lazy::new(|| CString::new("raas").unwrap()); static FORWARD_CHAIN_NAME: Lazy<CString> = Lazy::new(|| CString::new("forward").unwrap()); mod rule; -pub use rule::BlockRule; +pub use rule::{BlockRule, Endpoints}; #[derive(Default)] pub struct BlockList { diff --git a/ci/ios/test-router/raas/src/block_list/rule.rs b/ci/ios/test-router/raas/src/block_list/rule.rs index e6ce90f20c..6604e9c3fa 100644 --- a/ci/ios/test-router/raas/src/block_list/rule.rs +++ b/ci/ios/test-router/raas/src/block_list/rule.rs @@ -1,14 +1,25 @@ use crate::web::routes::TransportProtocol; use mnl::mnl_sys::libc; -use nftnl::{expr, nft_expr, Chain, Rule}; +use nftnl::{expr, nft_expr, nft_expr_payload, Chain, Rule}; -use std::{collections::BTreeSet, iter, net::IpAddr}; +use ipnetwork::IpNetwork; +use std::{collections::BTreeSet, iter}; #[derive(Clone, serde::Serialize)] -pub struct BlockRule { - pub src: IpAddr, - pub dst: IpAddr, - pub protocols: BTreeSet<TransportProtocol>, +pub enum BlockRule { + Host { + endpoints: Endpoints, + protocols: BTreeSet<TransportProtocol>, + }, + WireGuard { + endpoints: Endpoints, + }, +} + +#[derive(Clone, serde::Serialize)] +pub struct Endpoints { + pub src: IpNetwork, + pub dst: IpNetwork, } impl BlockRule { @@ -16,15 +27,15 @@ impl BlockRule { &'a self, chain: &'a Chain<'a>, ) -> Box<dyn Iterator<Item = Rule<'a>> + 'a> { - if self.protocols.is_empty() { - return Box::new(iter::once(self.create_nft_rule_inner(chain, None))); + match self { + BlockRule::Host { protocols, .. } if !protocols.is_empty() => { + let iter = protocols + .iter() + .map(|protocol| self.create_nft_rule_inner(chain, Some(*protocol))); + Box::new(iter) + } + _ => Box::new(iter::once(self.create_nft_rule_inner(chain, None))), } - - let iter = self - .protocols - .iter() - .map(|protocol| self.create_nft_rule_inner(chain, Some(*protocol))); - Box::new(iter) } fn create_nft_rule_inner<'a>( @@ -33,46 +44,42 @@ impl BlockRule { transport_protocol: Option<TransportProtocol>, ) -> Rule<'a> { let mut rule = Rule::new(chain); - check_l3proto(&mut rule, self.src); - if let Some(protocol) = transport_protocol { - check_l4proto(&mut rule, protocol); - }; - // Add source checking - rule.add_expr(match self.src { - IpAddr::V4(_) => &nft_expr!(payload ipv4 saddr), - IpAddr::V6(_) => &nft_expr!(payload ipv6 saddr), - }); - match self.src { - IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)), - IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)), - }; + match *self { + BlockRule::Host { + endpoints: Endpoints { src, dst }, + .. + } => { + check_l3proto(&mut rule, src); + if let Some(protocol) = transport_protocol { + check_l4proto(&mut rule, protocol); + }; + check_ip_addrs(&mut rule, src, dst); + } + BlockRule::WireGuard { + endpoints: Endpoints { src, dst }, + } => { + check_l3proto(&mut rule, src); + check_ip_addrs(&mut rule, src, dst); + check_wireguard_traffic(&mut rule); + } + } - // Add destination check - rule.add_expr(match self.dst { - IpAddr::V4(_) => &nft_expr!(payload ipv4 daddr), - IpAddr::V6(_) => &nft_expr!(payload ipv6 daddr), - }); - match self.dst { - IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)), - IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)), - }; rule.add_expr(&nft_expr!(counter)); rule.add_expr(&expr::Verdict::Drop); - rule } } -fn check_l3proto(rule: &mut Rule<'_>, ip: IpAddr) { +fn check_l3proto(rule: &mut Rule<'_>, ip: IpNetwork) { rule.add_expr(&nft_expr!(meta nfproto)); rule.add_expr(&nft_expr!(cmp == l3proto(ip))); } -fn l3proto(addr: IpAddr) -> u8 { +fn l3proto(addr: IpNetwork) -> u8 { match addr { - IpAddr::V4(_) => libc::NFPROTO_IPV4 as u8, - IpAddr::V6(_) => libc::NFPROTO_IPV6 as u8, + IpNetwork::V4(_) => libc::NFPROTO_IPV4 as u8, + IpNetwork::V6(_) => libc::NFPROTO_IPV6 as u8, } } @@ -80,3 +87,54 @@ fn check_l4proto(rule: &mut Rule<'_>, protocol: TransportProtocol) { rule.add_expr(&nft_expr!(meta l4proto)); rule.add_expr(&nft_expr!(cmp == protocol.as_ipproto())); } + +fn check_ip_addrs(rule: &mut Rule, src: IpNetwork, dst: IpNetwork) { + // Add source checking + rule.add_expr(match src { + IpNetwork::V4(_) => &nft_expr!(payload ipv4 saddr), + IpNetwork::V6(_) => &nft_expr!(payload ipv6 saddr), + }); + check_matches_prefix(rule, src); + + // Add destination check + rule.add_expr(match dst { + IpNetwork::V4(_) => &nft_expr!(payload ipv4 daddr), + IpNetwork::V6(_) => &nft_expr!(payload ipv6 daddr), + }); + check_matches_prefix(rule, dst); + + fn check_matches_prefix(rule: &mut Rule, network: IpNetwork) { + // Compute the bitwise AND of the incoming packet IP address and the mask, and then + // compare this value to the bitwise AND of the rule IP address and the mask. + // This will match when the network prefixes of the IP address to filter and rule + // IP address matches. E.g. the rule IP address/network 34.117.0.0/16 will match + // an incoming packet IP address 34.117.105.189. + match network { + IpNetwork::V4(addr) => { + rule.add_expr(&nft_expr!(bitwise mask addr.mask(), xor 0x0)); + rule.add_expr(&nft_expr!(cmp == addr.ip() & addr.mask())); + } + IpNetwork::V6(addr) => { + rule.add_expr(&nft_expr!(bitwise mask addr.mask(), xor 0x0)); + rule.add_expr(&nft_expr!(cmp == addr.ip() & addr.mask())); + } + }; + } +} + +fn check_wireguard_traffic(rule: &mut Rule) { + rule.add_expr(&nft_expr!(meta l4proto)); + rule.add_expr(&nft_expr!(cmp == libc::IPPROTO_UDP)); + + // UDP header is 8 bytes, after which we have the WireGuard header which is 4 bytes, + // where the first byte can be 1 to 4 (inclusive) and the last 3 bytes are 0. + // See: https://wiki.wireshark.org/WireGuard + rule.add_expr(&nft_expr_payload!(th 8, 1)); + rule.add_expr(&nft_expr!(cmp >= 1)); + + rule.add_expr(&nft_expr_payload!(th 8, 1)); + rule.add_expr(&nft_expr!(cmp <= 4)); + + rule.add_expr(&nft_expr_payload!(th 9, 3)); + rule.add_expr(&nft_expr!(cmp == 0)); +} diff --git a/ci/ios/test-router/raas/src/capture/mod.rs b/ci/ios/test-router/raas/src/capture/mod.rs index 992da8395a..c5c04ae82f 100644 --- a/ci/ios/test-router/raas/src/capture/mod.rs +++ b/ci/ios/test-router/raas/src/capture/mod.rs @@ -53,7 +53,7 @@ impl PacketCodec for CloneCodec { } } -const RAAS_TMP_DIR: &'static str = "raas"; +const RAAS_TMP_DIR: &str = "raas"; impl Capture { fn capture_file_path(label: &uuid::Uuid) -> PathBuf { @@ -94,8 +94,11 @@ impl Capture { let mut stream = capture.stream(CloneCodec).map_err(Error::CreateStream)?; let capture = tokio::spawn(async move { - let (pcap_tx, pcap_rx): (_, sync_mpsc::Receiver<(PacketHeader, Box<[u8]>)>) = - sync_mpsc::channel(); + #[allow(clippy::type_complexity)] + let (pcap_tx, pcap_rx): ( + _, + sync_mpsc::Receiver<(PacketHeader, Box<[u8]>)>, + ) = sync_mpsc::channel(); tokio::task::spawn_blocking(move || { while let Ok((header, data)) = pcap_rx.recv() { let packet = Packet { diff --git a/ci/ios/test-router/raas/src/capture/parse.rs b/ci/ios/test-router/raas/src/capture/parse.rs index 8da51d1f59..e8f96c280a 100644 --- a/ci/ios/test-router/raas/src/capture/parse.rs +++ b/ci/ios/test-router/raas/src/capture/parse.rs @@ -192,7 +192,7 @@ trait IpPacket: pnet_packet::Packet { } } -impl<'a> IpPacket for Ipv4Packet<'a> { +impl IpPacket for Ipv4Packet<'_> { fn source(&self) -> IpAddr { self.get_source().into() } @@ -206,7 +206,7 @@ impl<'a> IpPacket for Ipv4Packet<'a> { } } -impl<'a> IpPacket for Ipv6Packet<'a> { +impl IpPacket for Ipv6Packet<'_> { fn source(&self) -> IpAddr { self.get_source().into() } diff --git a/ci/ios/test-router/raas/src/web/routes.rs b/ci/ios/test-router/raas/src/web/routes.rs index a2fae72739..2dfec6f21a 100644 --- a/ci/ios/test-router/raas/src/web/routes.rs +++ b/ci/ios/test-router/raas/src/web/routes.rs @@ -1,27 +1,29 @@ -use std::{ - collections::{BTreeMap, BTreeSet}, - net::IpAddr, -}; - use axum::{ extract::{Json, Path, State}, http::StatusCode, response::IntoResponse, }; +use ipnetwork::IpNetwork; use mnl::mnl_sys::libc; +use std::collections::{BTreeMap, BTreeSet}; use uuid::Uuid; -use crate::block_list::BlockRule; +use crate::block_list::{BlockList, BlockRule, Endpoints}; +use crate::web; #[derive(serde::Deserialize, Clone)] pub struct NewRule { - pub src: IpAddr, - pub dst: IpAddr, + pub src: IpNetwork, + pub dst: IpNetwork, pub protocols: Option<BTreeSet<TransportProtocol>>, + #[serde(default)] + pub block_wireguard: bool, pub label: Uuid, } -#[derive(serde::Deserialize, serde::Serialize, PartialOrd, Ord, PartialEq, Eq, Clone, Copy)] +#[derive( + PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Debug, serde::Deserialize, serde::Serialize, +)] #[serde(rename_all = "snake_case")] pub enum TransportProtocol { Tcp, @@ -43,30 +45,29 @@ impl TransportProtocol { pub async fn add_rule( State(state): State<super::State>, - Json(rule): Json<NewRule>, + Json(json): Json<NewRule>, ) -> impl IntoResponse { - let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { - let label = rule.label; - let rule = BlockRule { - src: rule.src, - dst: rule.dst, - protocols: rule.protocols.unwrap_or_default(), - }; - let Ok(mut fw) = state.block_list.lock() else { - return Err(anyhow::anyhow!("Firewall thread panicked")); + let result = access_firewall(state, move |fw| { + let label = json.label; + let src = json.src; + let dst = json.dst; + + let rule = if json.block_wireguard { + BlockRule::WireGuard { + endpoints: Endpoints { src, dst }, + } + } else { + BlockRule::Host { + endpoints: Endpoints { src, dst }, + protocols: json.protocols.unwrap_or_default(), + } }; fw.add_rule(rule.clone(), label)?; - log::info!( - "Successfully added a rule to block {} from {} for test {}", - rule.src, - rule.dst, - label, - ); + log_rule(&rule, &label); Ok(()) }) - .await - .expect("failed to join blocking task"); + .await; respond_with_result(result, StatusCode::CREATED) } @@ -75,18 +76,28 @@ pub async fn delete_rules( Path(label): Path<Uuid>, State(state): State<super::State>, ) -> impl IntoResponse { - let result = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + let result = access_firewall(state, move |fw| { + fw.clear_rules_with_label(&label)?; + log::info!("Successfully removed all rules for test {label}"); + Ok(()) + }) + .await; + + respond_with_result(result, StatusCode::OK) +} + +pub async fn access_firewall<F>(state: web::State, run: F) -> anyhow::Result<()> +where + F: FnOnce(&mut BlockList) -> anyhow::Result<()> + Send + 'static, +{ + tokio::task::spawn_blocking(move || -> anyhow::Result<()> { let Ok(mut fw) = state.block_list.lock() else { return Err(anyhow::anyhow!("Firewall thread panicked")); }; - - fw.clear_rules_with_label(&label)?; - log::info!("Successfully removed all rules for test {label}",); - Ok(()) + run(&mut fw) }) .await - .expect("failed to join blocking task"); - respond_with_result(result, StatusCode::OK) + .expect("failed to join blocking task") } fn respond_with_result(result: anyhow::Result<()>, success_code: StatusCode) -> impl IntoResponse { @@ -108,7 +119,25 @@ pub async fn list_all_rules(State(state): State<super::State>) -> impl IntoRespo .expect("failed to join blocking task"); match all_rules { - Ok(all_rules) => axum::Json(all_rules).into_response(), + Ok(all_rules) => Json(all_rules).into_response(), Err(err) => (StatusCode::SERVICE_UNAVAILABLE, format!("{err}\n")).into_response(), } } + +fn log_rule(rule: &BlockRule, label: &Uuid) { + match rule { + BlockRule::Host { + protocols, + endpoints: Endpoints { src, dst }, + } => { + log::info!( + "Successfully added a rule to block {src} from {dst} for test {label} for protocols {protocols:?}", + ); + } + BlockRule::WireGuard { + endpoints: Endpoints { src, dst }, + } => { + log::info!("Successfully added a rule to block {src} from {dst} WireGuard traffic for test {label}",); + } + } +} diff --git a/ci/ios/test-router/router-config.nix b/ci/ios/test-router/router-config.nix index c10a817145..49c6d74614 100644 --- a/ci/ios/test-router/router-config.nix +++ b/ci/ios/test-router/router-config.nix @@ -33,7 +33,7 @@ in networking.hostName = args.hostname; networking.useDHCP = true; - system.stateVersion = "23.11"; + system.stateVersion = "24.11"; systemd.network.netdevs."1-lanBridge" = { netdevConfig = { |
