diff options
6 files changed, 208 insertions, 66 deletions
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/NetworkingProtocol.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/NetworkingProtocol.kt index 7cc12a9250..5e16c2e383 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/NetworkingProtocol.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/NetworkingProtocol.kt @@ -8,4 +8,5 @@ enum class NetworkingProtocol { @SerialName("tcp") TCP, @SerialName("udp") UDP, @SerialName("icmp") ICMP, + @SerialName("wireguard") WireGuard, } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt index 1431d9bf1b..341fe7b394 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt @@ -14,7 +14,6 @@ import net.mullvad.mullvadvpn.test.e2e.router.NetworkingProtocol data class DropRule( @SerialName("src") val source: String, @SerialName("dst") val destination: String, - @SerialName("block_wireguard") val blockWireGuard: Boolean = false, val protocols: List<NetworkingProtocol>, @EncodeDefault val label: String = "urn:uuid:${SessionIdentifier.fromDeviceIdentifier()}", ) { @@ -29,6 +28,6 @@ data class DropRule( } fun blockWireGuardTrafficRule(to: String): DropRule = - blockUDPTrafficRule(to).copy(blockWireGuard = true) + blockUDPTrafficRule(to).copy(protocols = listOf(NetworkingProtocol.WireGuard)) } } diff --git a/ci/ios/test-router/raas/.gitignore b/ci/ios/test-router/raas/.gitignore new file mode 100644 index 0000000000..2f7896d1d1 --- /dev/null +++ b/ci/ios/test-router/raas/.gitignore @@ -0,0 +1 @@ +target/ 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 82eece7bb1..cc89a28d50 100644 --- a/ci/ios/test-router/raas/src/block_list/mod.rs +++ b/ci/ios/test-router/raas/src/block_list/mod.rs @@ -14,11 +14,9 @@ pub struct BlockList { } impl BlockList { - pub fn add_rule(&mut self, rule: BlockRule, label: uuid::Uuid) -> io::Result<()> { - { - let rules = self.rules.entry(label).or_default(); - rules.push(rule); - } + pub fn add_rules(&mut self, rules: &[BlockRule], label: uuid::Uuid) -> io::Result<()> { + let rules_for_label = self.rules.entry(label).or_default(); + rules_for_label.extend_from_slice(rules); self.apply_rules() } @@ -89,6 +87,6 @@ impl BlockList { self.rules .values() .flatten() - .flat_map(move |rule| rule.create_nft_rule(chain)) + .flat_map(move |rule| rule.create_nft_rules(chain)) } } 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 6604e9c3fa..04ad57720d 100644 --- a/ci/ios/test-router/raas/src/block_list/rule.rs +++ b/ci/ios/test-router/raas/src/block_list/rule.rs @@ -3,7 +3,7 @@ use mnl::mnl_sys::libc; use nftnl::{expr, nft_expr, nft_expr_payload, Chain, Rule}; use ipnetwork::IpNetwork; -use std::{collections::BTreeSet, iter}; +use std::collections::BTreeSet; #[derive(Clone, serde::Serialize)] pub enum BlockRule { @@ -16,61 +16,119 @@ pub enum BlockRule { }, } -#[derive(Clone, serde::Serialize)] +#[derive(Clone, Copy, serde::Serialize)] pub struct Endpoints { pub src: IpNetwork, pub dst: IpNetwork, + /// Normally a packet sent to `dst` would match the block rule, but this option inverts that + /// so that any packet *not* sent to `dst` will match the block rule. + pub invert_dst: bool, } impl BlockRule { - pub fn create_nft_rule<'a>( - &'a self, - chain: &'a Chain<'a>, - ) -> Box<dyn Iterator<Item = Rule<'a>> + 'a> { - 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))), - } + /// Creates one or more nft rules that correspond to this BlockRule. The returned Vec will always + /// have at least one element. + pub fn create_nft_rules<'a>(&'a self, chain: &'a Chain<'a>) -> Vec<Rule<'a>> { + let rules = match self { + BlockRule::Host { protocols, .. } if !protocols.is_empty() => protocols + .iter() + .flat_map(|protocol| self.create_nft_rules_inner(chain, Some(*protocol))) + .collect(), + _ => self.create_nft_rules_inner(chain, None), + }; + assert!(!rules.is_empty()); + rules } - fn create_nft_rule_inner<'a>( + fn create_nft_rules_inner<'a>( &self, chain: &'a Chain<'a>, transport_protocol: Option<TransportProtocol>, - ) -> Rule<'a> { - let mut rule = Rule::new(chain); + ) -> Vec<Rule<'a>> { + let mut rules = Vec::new(); match *self { BlockRule::Host { - endpoints: Endpoints { src, dst }, + endpoints: + Endpoints { + src, + dst, + invert_dst, + }, .. } => { - check_l3proto(&mut rule, src); - if let Some(protocol) = transport_protocol { - check_l4proto(&mut rule, protocol); - }; - check_ip_addrs(&mut rule, src, dst); + let mut main_rule = nft_rule_host(chain, src, Some(dst), transport_protocol); + if invert_dst { + // Inverted case - accept all traffic that matches the main rule + main_rule.add_expr(&expr::Verdict::Accept); + rules.push(main_rule); + + // Block all other traffic from `src` that is not sent to `dst` + let mut block_rule = nft_rule_host(chain, src, None, transport_protocol); + block_rule.add_expr(&expr::Verdict::Drop); + rules.push(block_rule); + } else { + // Normal case - drop all traffic that matches the main rule + main_rule.add_expr(&expr::Verdict::Drop); + rules.push(main_rule); + } } BlockRule::WireGuard { - endpoints: Endpoints { src, dst }, + endpoints: + Endpoints { + src, + dst, + invert_dst, + }, } => { - check_l3proto(&mut rule, src); - check_ip_addrs(&mut rule, src, dst); - check_wireguard_traffic(&mut rule); + let mut main_rule = nft_rule_wireguard(chain, src, Some(dst)); + if invert_dst { + main_rule.add_expr(&expr::Verdict::Accept); + rules.push(main_rule); + + let mut block_rule = nft_rule_wireguard(chain, src, None); + block_rule.add_expr(&expr::Verdict::Drop); + rules.push(block_rule); + } else { + main_rule.add_expr(&expr::Verdict::Drop); + rules.push(main_rule); + } } } - rule.add_expr(&nft_expr!(counter)); - rule.add_expr(&expr::Verdict::Drop); - rule + rules } } +fn nft_rule_host<'a>( + chain: &'a Chain<'a>, + src: IpNetwork, + dst: Option<IpNetwork>, + transport_protocol: Option<TransportProtocol>, +) -> Rule<'a> { + let mut rule = Rule::new(chain); + check_l3proto(&mut rule, src); + if let Some(protocol) = transport_protocol { + check_l4proto(&mut rule, protocol); + }; + check_ip_addrs(&mut rule, src, dst); + rule.add_expr(&nft_expr!(counter)); + rule +} + +fn nft_rule_wireguard<'a>( + chain: &'a Chain<'a>, + src: IpNetwork, + dst: Option<IpNetwork>, +) -> Rule<'a> { + let mut rule = Rule::new(chain); + check_l3proto(&mut rule, src); + check_ip_addrs(&mut rule, src, dst); + check_wireguard_traffic(&mut rule); + rule.add_expr(&nft_expr!(counter)); + rule +} + fn check_l3proto(rule: &mut Rule<'_>, ip: IpNetwork) { rule.add_expr(&nft_expr!(meta nfproto)); rule.add_expr(&nft_expr!(cmp == l3proto(ip))); @@ -88,7 +146,7 @@ fn check_l4proto(rule: &mut Rule<'_>, protocol: TransportProtocol) { rule.add_expr(&nft_expr!(cmp == protocol.as_ipproto())); } -fn check_ip_addrs(rule: &mut Rule, src: IpNetwork, dst: IpNetwork) { +fn check_ip_addrs(rule: &mut Rule, src: IpNetwork, dst: Option<IpNetwork>) { // Add source checking rule.add_expr(match src { IpNetwork::V4(_) => &nft_expr!(payload ipv4 saddr), @@ -96,12 +154,14 @@ fn check_ip_addrs(rule: &mut Rule, src: IpNetwork, dst: IpNetwork) { }); 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); + // Add destination check if dst is given + if let Some(dst) = dst { + 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 diff --git a/ci/ios/test-router/raas/src/web/routes.rs b/ci/ios/test-router/raas/src/web/routes.rs index 2dfec6f21a..42c4b9687e 100644 --- a/ci/ios/test-router/raas/src/web/routes.rs +++ b/ci/ios/test-router/raas/src/web/routes.rs @@ -8,17 +8,38 @@ use mnl::mnl_sys::libc; use std::collections::{BTreeMap, BTreeSet}; use uuid::Uuid; -use crate::block_list::{BlockList, BlockRule, Endpoints}; -use crate::web; +use crate::{ + block_list::{BlockList, BlockRule, Endpoints}, + web, +}; #[derive(serde::Deserialize, Clone)] pub struct NewRule { + /// A packet that is sent *from* `src` will match the block rule. pub src: IpNetwork, + /// A packet that is sent *to* `dst` will match the block rule. pub dst: IpNetwork, - pub protocols: Option<BTreeSet<TransportProtocol>>, + /// A list of protocols that should be blocked, e.g. Tcp or WireGuard. The default behavior + /// is to block all traffic regardless of protocol, but if `protocols` is non-empty, only + /// traffic that uses that protocol is blocked. #[serde(default)] - pub block_wireguard: bool, + pub protocols: BTreeSet<Protocol>, + /// A unique identifier for a group of rules. It is possible to add rules to an existing label + /// and to remove all rules for a label. pub label: Uuid, + /// Normally a packet sent to `dst` would match the block rule, but this option inverts that + /// so that any packet *not* sent to `dst` will match the block rule. + #[serde(default)] + pub block_all_except_dst: bool, +} + +#[derive( + PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Debug, serde::Deserialize, serde::Serialize, +)] +#[serde(untagged)] +pub enum Protocol { + Transport(TransportProtocol), + Application(AppProtocol), } #[derive( @@ -43,28 +64,72 @@ impl TransportProtocol { } } +#[derive( + PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Debug, serde::Deserialize, serde::Serialize, +)] +#[serde(rename_all = "snake_case")] +pub enum AppProtocol { + #[serde(rename = "wireguard")] + WireGuard, +} + +impl AppProtocol { + // Each "app protocol" (e.g. WireGuard, more could be added in the future) is mapped to its + // own block rule, as opposed to the transport protocols which have a single rule. + // This is to support each app protocol being able to have a unique block criteria + // (e.g. WireGuard needs to block UDP packets that match a certain pattern). + fn as_block_rule(&self, endpoints: Endpoints) -> BlockRule { + match self { + AppProtocol::WireGuard => BlockRule::WireGuard { endpoints }, + } + } +} + pub async fn add_rule( State(state): State<super::State>, Json(json): Json<NewRule>, ) -> impl IntoResponse { let result = access_firewall(state, move |fw| { let label = json.label; - let src = json.src; - let dst = json.dst; + let endpoints = Endpoints { + src: json.src, + dst: json.dst, + invert_dst: json.block_all_except_dst, + }; + let protocols = json.protocols; - let rule = if json.block_wireguard { - BlockRule::WireGuard { - endpoints: Endpoints { src, dst }, - } + let mut block_rules = vec![]; + + if protocols.is_empty() { + // If no protocols are specified we default to blocking everything for (src, dst). + block_rules.push(BlockRule::Host { + endpoints, + protocols: BTreeSet::new(), + }); } else { - BlockRule::Host { - endpoints: Endpoints { src, dst }, - protocols: json.protocols.unwrap_or_default(), + let mut transport = BTreeSet::new(); + let mut application = BTreeSet::new(); + for protocol in protocols { + match protocol { + Protocol::Transport(p) => transport.insert(p), + Protocol::Application(p) => application.insert(p), + }; } - }; + if !transport.is_empty() { + block_rules.push(BlockRule::Host { + endpoints, + protocols: transport, + }); + } + for protocol in application { + block_rules.push(protocol.as_block_rule(endpoints)); + } + } - fw.add_rule(rule.clone(), label)?; - log_rule(&rule, &label); + fw.add_rules(&block_rules, label)?; + for rule in block_rules { + log_rule(&rule, &label); + } Ok(()) }) .await; @@ -128,16 +193,34 @@ fn log_rule(rule: &BlockRule, label: &Uuid) { match rule { BlockRule::Host { protocols, - endpoints: Endpoints { src, dst }, + endpoints: + Endpoints { + src, + dst, + invert_dst, + }, } => { log::info!( - "Successfully added a rule to block {src} from {dst} for test {label} for protocols {protocols:?}", + "Successfully added a rule to {} {src} to {dst} for protocols {protocols:?} [test: {label}]", + if *invert_dst { "allow only traffic from" } else { "block" }, ); } BlockRule::WireGuard { - endpoints: Endpoints { src, dst }, + endpoints: + Endpoints { + src, + dst, + invert_dst, + }, } => { - log::info!("Successfully added a rule to block {src} from {dst} WireGuard traffic for test {label}",); + log::info!( + "Successfully added a rule to {} {src} to {dst} for WireGuard [test: {label}]", + if *invert_dst { + "allow only traffic from" + } else { + "block" + }, + ); } } } |
