summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKalle Lindström <karl.lindstrom@mullvad.net>2025-03-18 08:49:21 +0100
committerDavid Göransson <david.goransson@mullvad.net>2025-03-31 12:13:18 +0200
commitc93339781c1fec5b9db5cb81cfcbb478c8311c18 (patch)
tree7ae2f6bae8622156607ebaa5fef2b4acddd29ba8
parent12b98273d2d3057dfa30d2367d80d3504f3b11e9 (diff)
downloadmullvadvpn-c93339781c1fec5b9db5cb81cfcbb478c8311c18.tar.xz
mullvadvpn-c93339781c1fec5b9db5cb81cfcbb478c8311c18.zip
Add ability to block wireguard traffic
-rw-r--r--ci/ios/test-router/raas/src/block_list/mod.rs2
-rw-r--r--ci/ios/test-router/raas/src/block_list/rule.rs113
-rw-r--r--ci/ios/test-router/raas/src/web/routes.rs97
3 files changed, 144 insertions, 68 deletions
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..5bd876fe96 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,24 @@
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};
#[derive(Clone, serde::Serialize)]
-pub struct BlockRule {
+pub enum BlockRule {
+ Host {
+ endpoints: Endpoints,
+ protocols: BTreeSet<TransportProtocol>,
+ },
+ WireGuard {
+ endpoints: Endpoints,
+ },
+}
+
+#[derive(Clone, serde::Serialize)]
+pub struct Endpoints {
pub src: IpAddr,
pub dst: IpAddr,
- pub protocols: BTreeSet<TransportProtocol>,
}
impl BlockRule {
@@ -16,15 +26,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,33 +43,29 @@ 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
}
}
@@ -80,3 +86,42 @@ 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: IpAddr, dst: IpAddr) {
+ // Add source checking
+ rule.add_expr(match src {
+ IpAddr::V4(_) => &nft_expr!(payload ipv4 saddr),
+ IpAddr::V6(_) => &nft_expr!(payload ipv6 saddr),
+ });
+ match src {
+ IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ };
+
+ // Add destination check
+ rule.add_expr(match dst {
+ IpAddr::V4(_) => &nft_expr!(payload ipv4 daddr),
+ IpAddr::V6(_) => &nft_expr!(payload ipv6 daddr),
+ });
+ match dst {
+ IpAddr::V4(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ IpAddr::V6(addr) => rule.add_expr(&nft_expr!(cmp == addr)),
+ };
+}
+
+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/web/routes.rs b/ci/ios/test-router/raas/src/web/routes.rs
index a2fae72739..2b6432c5a2 100644
--- a/ci/ios/test-router/raas/src/web/routes.rs
+++ b/ci/ios/test-router/raas/src/web/routes.rs
@@ -1,28 +1,32 @@
-use std::{
- collections::{BTreeMap, BTreeSet},
- net::IpAddr,
-};
-
use axum::{
extract::{Json, Path, State},
http::StatusCode,
response::IntoResponse,
};
use mnl::mnl_sys::libc;
+use std::sync::MutexGuard;
+use std::{
+ collections::{BTreeMap, BTreeSet},
+ net::IpAddr,
+};
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 protocols: Option<BTreeSet<TransportProtocol>>,
+ #[serde(default)]
+ pub block_wireguard: bool,
pub label: Uuid,
}
#[derive(serde::Deserialize, serde::Serialize, PartialOrd, Ord, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "snake_case")]
+#[derive(Debug)]
pub enum TransportProtocol {
Tcp,
Udp,
@@ -43,30 +47,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 |mut 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,20 +78,30 @@ 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 Ok(mut fw) = state.block_list.lock() else {
- return Err(anyhow::anyhow!("Firewall thread panicked"));
- };
-
+ let result = access_firewall(state, move |mut fw| {
fw.clear_rules_with_label(&label)?;
- log::info!("Successfully removed all rules for test {label}",);
+ log::info!("Successfully removed all rules for test {label}");
Ok(())
})
- .await
- .expect("failed to join blocking task");
+ .await;
+
respond_with_result(result, StatusCode::OK)
}
+pub async fn access_firewall<F>(state: web::State, run: F) -> anyhow::Result<()>
+where
+ F: FnOnce(MutexGuard<BlockList>) -> anyhow::Result<()> + Send + 'static,
+{
+ tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
+ let Ok(fw) = state.block_list.lock() else {
+ return Err(anyhow::anyhow!("Firewall thread panicked"));
+ };
+ run(fw)
+ })
+ .await
+ .expect("failed to join blocking task")
+}
+
fn respond_with_result(result: anyhow::Result<()>, success_code: StatusCode) -> impl IntoResponse {
match result {
Ok(_) => (success_code, String::new()),
@@ -108,7 +121,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}",);
+ }
+ }
+}