summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-16 14:33:35 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-16 14:33:35 +0200
commit940d155db3ff538df611cc93113db18ebadca212 (patch)
tree7df524a6dab4c01aef5fc7f9f57c1c8a614cc70f
parent13615c74fef1dd48876f53cc052465f633792ee8 (diff)
parent7ed99ce3f76536c6146f0e7d5013af8add6ef918 (diff)
downloadmullvadvpn-940d155db3ff538df611cc93113db18ebadca212.tar.xz
mullvadvpn-940d155db3ff538df611cc93113db18ebadca212.zip
Merge branch 'add-block-all-except-destination-api-to-raas'
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/NetworkingProtocol.kt1
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/router/firewall/DropRule.kt3
-rw-r--r--ci/ios/test-router/raas/.gitignore1
-rw-r--r--ci/ios/test-router/raas/src/block_list/mod.rs10
-rw-r--r--ci/ios/test-router/raas/src/block_list/rule.rs136
-rw-r--r--ci/ios/test-router/raas/src/web/routes.rs123
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"
+ },
+ );
}
}
}