summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-08-14 11:29:51 +0200
committerDavid Lönnhager <david.l@mullvad.net>2025-08-21 14:58:30 +0200
commite3d0cab9bbb56af9a1e92da0a5040365bb0917d3 (patch)
tree7f2da7c81efd87fc7babe93763f606afb7936895
parentdcb3f3d11b0ede581800d7a1a63ff56ee636b0bb (diff)
downloadmullvadvpn-e3d0cab9bbb56af9a1e92da0a5040365bb0917d3.tar.xz
mullvadvpn-e3d0cab9bbb56af9a1e92da0a5040365bb0917d3.zip
Block traffic to exit hop from non-relay client process in WinFw
This fixes an issue where traffic could leak unencrypted from the entry hop to the exit hop IP when using multihop
-rw-r--r--talpid-core/src/firewall/mod.rs6
-rw-r--r--talpid-core/src/firewall/windows/mod.rs25
-rw-r--r--talpid-core/src/firewall/windows/winfw/mod.rs34
-rw-r--r--talpid-core/src/firewall/windows/winfw/sys.rs2
-rw-r--r--talpid-core/src/tunnel_state_machine/connected_state.rs11
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs8
-rw-r--r--windows/winfw/src/winfw/fwcontext.cpp34
-rw-r--r--windows/winfw/src/winfw/fwcontext.h2
-rw-r--r--windows/winfw/src/winfw/mullvadguids.cpp62
-rw-r--r--windows/winfw/src/winfw/mullvadguids.h4
-rw-r--r--windows/winfw/src/winfw/rules/baseline/permitvpntunnel.cpp133
-rw-r--r--windows/winfw/src/winfw/rules/baseline/permitvpntunnel.h11
-rw-r--r--windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.cpp132
-rw-r--r--windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.h7
-rw-r--r--windows/winfw/src/winfw/winfw.cpp20
-rw-r--r--windows/winfw/src/winfw/winfw.h13
16 files changed, 465 insertions, 39 deletions
diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs
index d72a70083e..22d4954d64 100644
--- a/talpid-core/src/firewall/mod.rs
+++ b/talpid-core/src/firewall/mod.rs
@@ -83,6 +83,9 @@ pub enum FirewallPolicy {
Connecting {
/// The peer endpoint that should be allowed.
peer_endpoint: AllowedEndpoint,
+ /// IP of the exit endpoint, iff it differs from `peer_endpoint`
+ #[cfg(target_os = "windows")]
+ exit_endpoint_ip: Option<IpAddr>,
/// Metadata about the tunnel and tunnel interface.
tunnel: Option<crate::tunnel::TunnelMetadata>,
/// Flag setting if communication with LAN networks should be possible.
@@ -100,6 +103,9 @@ pub enum FirewallPolicy {
Connected {
/// The peer endpoint that should be allowed.
peer_endpoint: AllowedEndpoint,
+ /// IP of the exit endpoint, iff it differs from `peer_endpoint`
+ #[cfg(target_os = "windows")]
+ exit_endpoint_ip: Option<IpAddr>,
/// Metadata about the tunnel and tunnel interface.
tunnel: crate::tunnel::TunnelMetadata,
/// Flag setting if communication with LAN networks should be possible.
diff --git a/talpid-core/src/firewall/windows/mod.rs b/talpid-core/src/firewall/windows/mod.rs
index ad9af12df3..20d8901c4a 100644
--- a/talpid-core/src/firewall/windows/mod.rs
+++ b/talpid-core/src/firewall/windows/mod.rs
@@ -125,15 +125,16 @@ impl Firewall {
let apply_result = match policy {
FirewallPolicy::Connecting {
peer_endpoint,
+ exit_endpoint_ip,
tunnel,
allow_lan,
allowed_endpoint,
allowed_tunnel_traffic,
} => {
let cfg = &WinFwSettings::new(allow_lan);
-
self.set_connecting_state(
&peer_endpoint,
+ exit_endpoint_ip,
cfg,
tunnel.as_ref(),
allowed_endpoint,
@@ -142,12 +143,19 @@ impl Firewall {
}
FirewallPolicy::Connected {
peer_endpoint,
+ exit_endpoint_ip,
tunnel,
allow_lan,
dns_config,
} => {
let cfg = &WinFwSettings::new(allow_lan);
- self.set_connected_state(&peer_endpoint, cfg, &tunnel, &dns_config)
+ self.set_connected_state(
+ &peer_endpoint,
+ exit_endpoint_ip,
+ cfg,
+ &tunnel,
+ &dns_config,
+ )
}
FirewallPolicy::Blocked {
allow_lan,
@@ -192,6 +200,7 @@ impl Firewall {
fn set_connecting_state(
&mut self,
peer_endpoint: &AllowedEndpoint,
+ exit_endpoint_ip: Option<IpAddr>,
winfw_settings: &WinFwSettings,
tunnel_metadata: Option<&TunnelMetadata>,
allowed_endpoint: AllowedEndpoint,
@@ -201,6 +210,7 @@ impl Firewall {
let tunnel_interface = tunnel_metadata.map(|metadata| metadata.interface.as_ref());
winfw::apply_policy_connecting(
peer_endpoint,
+ exit_endpoint_ip,
winfw_settings,
tunnel_interface,
allowed_endpoint,
@@ -212,14 +222,21 @@ impl Firewall {
fn set_connected_state(
&mut self,
endpoint: &AllowedEndpoint,
+ exit_endpoint_ip: Option<IpAddr>,
winfw_settings: &WinFwSettings,
tunnel_metadata: &TunnelMetadata,
dns_config: &ResolvedDnsConfig,
) -> Result<(), Error> {
log::trace!("Applying 'connected' firewall policy");
let tunnel_interface = &tunnel_metadata.interface;
- winfw::apply_policy_connected(endpoint, winfw_settings, tunnel_interface, dns_config)
- .map_err(Error::ApplyingConnectedPolicy)
+ winfw::apply_policy_connected(
+ endpoint,
+ exit_endpoint_ip,
+ winfw_settings,
+ tunnel_interface,
+ dns_config,
+ )
+ .map_err(Error::ApplyingConnectedPolicy)
}
fn set_blocked_state(
diff --git a/talpid-core/src/firewall/windows/winfw/mod.rs b/talpid-core/src/firewall/windows/winfw/mod.rs
index b6850754a8..9a2d5a7c8a 100644
--- a/talpid-core/src/firewall/windows/winfw/mod.rs
+++ b/talpid-core/src/firewall/windows/winfw/mod.rs
@@ -1,7 +1,7 @@
//! Safe bindings for the WinFW library.
use super::{AllowedEndpoint, AllowedTunnelTraffic, Error, WideCString, widestring_ip};
-use std::ptr;
+use std::{net::IpAddr, ptr};
use talpid_types::{net::TransportProtocol, tunnel::FirewallPolicyError};
mod sys;
@@ -78,10 +78,7 @@ pub(super) fn apply_policy_blocked(
let allowed_endpoint = allowed_endpoint
.as_ref()
.map(WinFwAllowedEndpointContainer::as_endpoint);
- let allowed_endpoint_ptr = allowed_endpoint
- .as_ref()
- .map(ptr::from_ref)
- .unwrap_or(ptr::null());
+ let allowed_endpoint_ptr = allowed_endpoint.as_ref().map_or(ptr::null(), ptr::from_ref);
// SAFETY: This function is always safe to call
let application = unsafe { WinFw_ApplyPolicyBlocked(winfw_settings, allowed_endpoint_ptr) };
application.into_result()
@@ -89,6 +86,7 @@ pub(super) fn apply_policy_blocked(
pub(super) fn apply_policy_connecting(
peer_endpoint: &AllowedEndpoint,
+ exit_endpoint_ip: Option<IpAddr>,
winfw_settings: &WinFwSettings,
tunnel_interface: Option<&str>,
allowed_endpoint: AllowedEndpoint,
@@ -101,9 +99,6 @@ pub(super) fn apply_policy_connecting(
protocol: WinFwProt::from(peer_endpoint.endpoint.protocol),
};
- // SAFETY: `endpoint1_ip`, `endpoint2_ip`, `endpoint1`, `endpoint2`, `relay_client_wstrs`
- // must not be dropped until `WinFw_ApplyPolicyConnecting` has returned.
-
let relay_client_wstrs: Vec<_> = peer_endpoint
.clients
.iter()
@@ -171,11 +166,17 @@ pub(super) fn apply_policy_connecting(
.unwrap_or(ptr::null()),
};
+ let exit_endpoint_ip_wstr = exit_endpoint_ip.map(widestring_ip);
+ let exit_endpoint_ip_ptr = exit_endpoint_ip_wstr
+ .as_ref()
+ .map_or(ptr::null(), |ip| ip.as_ptr());
+
#[allow(clippy::undocumented_unsafe_blocks)] // Remove me if you dare.
let res = unsafe {
WinFw_ApplyPolicyConnecting(
winfw_settings,
&winfw_relay,
+ exit_endpoint_ip_ptr,
relay_client_wstr_ptrs.as_ptr(),
relay_client_wstr_ptrs_len,
interface_wstr_ptr,
@@ -183,9 +184,7 @@ pub(super) fn apply_policy_connecting(
&allowed_tunnel_traffic,
)
};
- // SAFETY: All of these hold stack allocated memory which is pointed to by
- // `allowed_tunnel_traffic` and must remain allocated until `WinFw_ApplyPolicyConnecting`
- // has returned.
+ // SAFETY: All of these must remain allocated until `WinFw_ApplyPolicyConnecting` has returned.
drop(endpoint1_ip);
drop(endpoint2_ip);
#[allow(clippy::drop_non_drop)]
@@ -193,11 +192,13 @@ pub(super) fn apply_policy_connecting(
#[allow(clippy::drop_non_drop)]
drop(endpoint2);
drop(relay_client_wstrs);
+ drop(exit_endpoint_ip_wstr);
res.into_result()
}
pub(super) fn apply_policy_connected(
endpoint: &AllowedEndpoint,
+ exit_endpoint_ip: Option<IpAddr>,
winfw_settings: &WinFwSettings,
tunnel_interface: &str,
dns_config: &crate::dns::ResolvedDnsConfig,
@@ -245,11 +246,17 @@ pub(super) fn apply_policy_connected(
.map(|ip| ip.as_ptr())
.collect();
+ let exit_endpoint_ip_wstr = exit_endpoint_ip.map(widestring_ip);
+ let exit_endpoint_ip_ptr = exit_endpoint_ip_wstr
+ .as_ref()
+ .map_or(ptr::null(), |ip| ip.as_ptr());
+
#[allow(clippy::undocumented_unsafe_blocks)] // Remove me if you dare.
let result = unsafe {
WinFw_ApplyPolicyConnected(
winfw_settings,
&winfw_relay,
+ exit_endpoint_ip_ptr,
relay_client_wstr_ptrs.as_ptr(),
relay_client_wstr_ptrs_len,
tunnel_alias.as_ptr(),
@@ -260,8 +267,9 @@ pub(super) fn apply_policy_connected(
)
};
- // SAFETY: `relay_client_wstrs` holds memory pointed to by pointers used in C++ and must
- // not be dropped until after `WinFw_ApplyPolicyConnected` has returned.
+ // SAFETY: All of these must remain allocated until `WinFw_ApplyPolicyConnected` has returned.
+ drop(exit_endpoint_ip_wstr);
+ drop(ip_str);
drop(relay_client_wstrs);
result.into_result()
}
diff --git a/talpid-core/src/firewall/windows/winfw/sys.rs b/talpid-core/src/firewall/windows/winfw/sys.rs
index 964f4c4d51..c320181d75 100644
--- a/talpid-core/src/firewall/windows/winfw/sys.rs
+++ b/talpid-core/src/firewall/windows/winfw/sys.rs
@@ -134,6 +134,7 @@ unsafe extern "system" {
pub fn WinFw_ApplyPolicyConnecting(
settings: &WinFwSettings,
relay: &WinFwEndpoint,
+ exitEndpointIp: *const libc::wchar_t,
relayClient: *const *const libc::wchar_t,
relayClientLen: usize,
tunnelIfaceAlias: *const libc::wchar_t,
@@ -145,6 +146,7 @@ unsafe extern "system" {
pub fn WinFw_ApplyPolicyConnected(
settings: &WinFwSettings,
relay: &WinFwEndpoint,
+ exitEndpointIp: *const libc::wchar_t,
relayClient: *const *const libc::wchar_t,
relayClientLen: usize,
tunnelIfaceAlias: *const libc::wchar_t,
diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs
index b671095fb3..12767d7d65 100644
--- a/talpid-core/src/tunnel_state_machine/connected_state.rs
+++ b/talpid-core/src/tunnel_state_machine/connected_state.rs
@@ -124,6 +124,15 @@ impl ConnectedState {
AllowedClients::Root
};
+ #[cfg(target_os = "windows")]
+ let exit_endpoint_ip = self
+ .tunnel_parameters
+ .get_exit_hop_endpoint()
+ .map(|ep| ep.address.ip());
+
+ #[cfg(target_os = "windows")]
+ debug_assert_ne!(exit_endpoint_ip, Some(endpoint.address.ip()));
+
let peer_endpoint = AllowedEndpoint { endpoint, clients };
#[cfg(target_os = "macos")]
@@ -133,6 +142,8 @@ impl ConnectedState {
FirewallPolicy::Connected {
peer_endpoint,
+ #[cfg(target_os = "windows")]
+ exit_endpoint_ip,
tunnel: self.metadata.clone(),
allow_lan: shared_values.allow_lan,
#[cfg(not(target_os = "android"))]
diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs
index 8f26c92b9d..9c89b3fc3d 100644
--- a/talpid-core/src/tunnel_state_machine/connecting_state.rs
+++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs
@@ -183,6 +183,12 @@ impl ConnectingState {
AllowedClients::Root
};
+ #[cfg(target_os = "windows")]
+ let exit_endpoint_ip = params.get_exit_hop_endpoint().map(|ep| ep.address.ip());
+
+ #[cfg(target_os = "windows")]
+ debug_assert_ne!(exit_endpoint_ip, Some(endpoint.address.ip()));
+
let peer_endpoint = AllowedEndpoint { endpoint, clients };
#[cfg(target_os = "macos")]
@@ -192,6 +198,8 @@ impl ConnectingState {
let policy = FirewallPolicy::Connecting {
peer_endpoint,
+ #[cfg(target_os = "windows")]
+ exit_endpoint_ip,
tunnel: tunnel_metadata.clone(),
allow_lan: shared_values.allow_lan,
allowed_endpoint: shared_values.allowed_endpoint.clone(),
diff --git a/windows/winfw/src/winfw/fwcontext.cpp b/windows/winfw/src/winfw/fwcontext.cpp
index 4ed22737fc..7747d7c822 100644
--- a/windows/winfw/src/winfw/fwcontext.cpp
+++ b/windows/winfw/src/winfw/fwcontext.cpp
@@ -185,6 +185,7 @@ bool FwContext::applyPolicyConnecting
(
const WinFwSettings &settings,
const WinFwEndpoint &relay,
+ const std::optional<wfp::IpAddress> &exitEndpointIp,
const std::vector<std::wstring> &relayClients,
const std::optional<std::wstring> &tunnelInterfaceAlias,
const std::optional<WinFwAllowedEndpoint> &allowedEndpoint,
@@ -209,12 +210,16 @@ bool FwContext::applyPolicyConnecting
case WinFwAllowedTunnelTrafficType::All:
{
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnel>(
+ relayClients,
*tunnelInterfaceAlias,
- std::nullopt
+ std::nullopt,
+ exitEndpointIp
));
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnelService>(
+ relayClients,
*tunnelInterfaceAlias,
- std::nullopt
+ std::nullopt,
+ exitEndpointIp
));
break;
}
@@ -229,12 +234,16 @@ bool FwContext::applyPolicyConnecting
std::nullopt,
});
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnel>(
+ relayClients,
*tunnelInterfaceAlias,
- onlyEndpoint
+ onlyEndpoint,
+ exitEndpointIp
));
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnelService>(
+ relayClients,
*tunnelInterfaceAlias,
- onlyEndpoint
+ onlyEndpoint,
+ exitEndpointIp
));
break;
}
@@ -253,12 +262,16 @@ bool FwContext::applyPolicyConnecting
})
});
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnel>(
+ relayClients,
*tunnelInterfaceAlias,
- endpoints
+ endpoints,
+ exitEndpointIp
));
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnelService>(
+ relayClients,
*tunnelInterfaceAlias,
- endpoints
+ endpoints,
+ exitEndpointIp
));
break;
}
@@ -280,6 +293,7 @@ bool FwContext::applyPolicyConnected
(
const WinFwSettings &settings,
const WinFwEndpoint &relay,
+ const std::optional<wfp::IpAddress> &exitEndpointIp,
const std::vector<std::wstring> &relayClient,
const std::wstring &tunnelInterfaceAlias,
const std::vector<wfp::IpAddress> &tunnelDnsServers,
@@ -306,13 +320,17 @@ bool FwContext::applyPolicyConnected
}
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnel>(
+ relayClient,
tunnelInterfaceAlias,
- std::nullopt
+ std::nullopt,
+ exitEndpointIp
));
ruleset.emplace_back(std::make_unique<baseline::PermitVpnTunnelService>(
+ relayClient,
tunnelInterfaceAlias,
- std::nullopt
+ std::nullopt,
+ exitEndpointIp
));
const auto status = applyRuleset(ruleset);
diff --git a/windows/winfw/src/winfw/fwcontext.h b/windows/winfw/src/winfw/fwcontext.h
index 92ecce4f4f..8a4e4f0301 100644
--- a/windows/winfw/src/winfw/fwcontext.h
+++ b/windows/winfw/src/winfw/fwcontext.h
@@ -28,6 +28,7 @@ public:
(
const WinFwSettings &settings,
const WinFwEndpoint &relay,
+ const std::optional<wfp::IpAddress> &exitEndpointIp,
const std::vector<std::wstring> &relayClients,
const std::optional<std::wstring> &tunnelInterfaceAlias,
const std::optional<WinFwAllowedEndpoint> &allowedEndpoint,
@@ -38,6 +39,7 @@ public:
(
const WinFwSettings &settings,
const WinFwEndpoint &relay,
+ const std::optional<wfp::IpAddress> &exitEndpointIp,
const std::vector<std::wstring> &relayClients,
const std::wstring &tunnelInterfaceAlias,
const std::vector<wfp::IpAddress> &tunnelDnsServers,
diff --git a/windows/winfw/src/winfw/mullvadguids.cpp b/windows/winfw/src/winfw/mullvadguids.cpp
index 890c8b0c2e..3029607a11 100644
--- a/windows/winfw/src/winfw/mullvadguids.cpp
+++ b/windows/winfw/src/winfw/mullvadguids.cpp
@@ -134,10 +134,14 @@ MullvadGuids::DetailedIdentityRegistry MullvadGuids::DetailedRegistry(IdentityQu
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_1()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnel_Outbound_Ipv4_2()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_2()));
+ registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnel_ExitIp()));
+ registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnel_BlockExitIp()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnelService_Ipv4_1()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnelService_Ipv6_1()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnelService_Ipv4_2()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnelService_Ipv6_2()));
+ registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnelService_ExitIp()));
+ registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitVpnTunnelService_BlockExitIp()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitNdp_Outbound_Router_Solicitation()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitNdp_Inbound_Router_Advertisement()));
registry.insert(std::make_pair(WfpObjectType::Filter, Filter_Baseline_PermitNdp_Outbound_Neighbor_Solicitation()));
@@ -723,6 +727,34 @@ const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_2()
}
//static
+const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnel_ExitIp()
+{
+ static const GUID g =
+ {
+ 0x1769c66f,
+ 0xa5ef,
+ 0x1128,
+ { 0xb1, 0x5e, 0xd9, 0xe2, 0x6e, 0xf4, 0xf0, 0x06 }
+ };
+
+ return g;
+}
+
+//static
+const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnel_BlockExitIp()
+{
+ static const GUID g =
+ {
+ 0x276bc66f,
+ 0xf5ef,
+ 0x1128,
+ { 0xb1, 0x5e, 0xd9, 0xe2, 0x6e, 0xf4, 0xf0, 0x06 }
+ };
+
+ return g;
+}
+
+//static
const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnelService_Ipv4_1()
{
static const GUID g =
@@ -780,6 +812,36 @@ const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnelService_Ipv6_2()
}
//static
+const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnelService_ExitIp()
+{
+
+ static const GUID g =
+ {
+ 0x01deb41a,
+ 0xb25d,
+ 0x1e60,
+ { 0xef, 0x12, 0xef, 0x3b, 0x40, 0xc0, 0x8e, 0xdc }
+ };
+
+ return g;
+}
+
+//static
+const GUID &MullvadGuids::Filter_Baseline_PermitVpnTunnelService_BlockExitIp()
+{
+
+ static const GUID g =
+ {
+ 0x01deb2b8,
+ 0xb25d,
+ 0x1e60,
+ { 0xef, 0x52, 0xef, 0x3b, 0x40, 0xc0, 0x8e, 0xdc }
+ };
+
+ return g;
+}
+
+//static
const GUID &MullvadGuids::Filter_Baseline_PermitNdp_Outbound_Router_Solicitation()
{
static const GUID g =
diff --git a/windows/winfw/src/winfw/mullvadguids.h b/windows/winfw/src/winfw/mullvadguids.h
index d63af0206d..e2fd7ab276 100644
--- a/windows/winfw/src/winfw/mullvadguids.h
+++ b/windows/winfw/src/winfw/mullvadguids.h
@@ -75,11 +75,15 @@ public:
static const GUID &Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_1();
static const GUID &Filter_Baseline_PermitVpnTunnel_Outbound_Ipv4_2();
static const GUID &Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_2();
+ static const GUID &Filter_Baseline_PermitVpnTunnel_ExitIp();
+ static const GUID &Filter_Baseline_PermitVpnTunnel_BlockExitIp();
static const GUID &Filter_Baseline_PermitVpnTunnelService_Ipv4_1();
static const GUID &Filter_Baseline_PermitVpnTunnelService_Ipv6_1();
static const GUID &Filter_Baseline_PermitVpnTunnelService_Ipv4_2();
static const GUID &Filter_Baseline_PermitVpnTunnelService_Ipv6_2();
+ static const GUID &Filter_Baseline_PermitVpnTunnelService_ExitIp();
+ static const GUID &Filter_Baseline_PermitVpnTunnelService_BlockExitIp();
static const GUID &Filter_Baseline_PermitNdp_Outbound_Router_Solicitation();
static const GUID &Filter_Baseline_PermitNdp_Inbound_Router_Advertisement();
diff --git a/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.cpp b/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.cpp
index 3aaff601a3..5ee9c2d966 100644
--- a/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.cpp
+++ b/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.cpp
@@ -4,6 +4,8 @@
#include <winfw/rules/shared.h>
#include <libwfp/filterbuilder.h>
#include <libwfp/conditionbuilder.h>
+#include <libwfp/conditions/comparison.h>
+#include <libwfp/conditions/conditionapplication.h>
#include <libwfp/conditions/conditioninterface.h>
#include <libwfp/conditions/conditionip.h>
#include <libwfp/conditions/conditionport.h>
@@ -16,11 +18,15 @@ namespace rules::baseline
{
PermitVpnTunnel::PermitVpnTunnel(
+ const std::vector<std::wstring> &relayClients,
const std::wstring &tunnelInterfaceAlias,
- const std::optional<Endpoints> &potentialEndpoints
+ const std::optional<Endpoints> &potentialEndpoints,
+ const std::optional<wfp::IpAddress> &exitEndpointIp
)
- : m_tunnelInterfaceAlias(tunnelInterfaceAlias)
+ : m_relayClients(relayClients)
+ , m_tunnelInterfaceAlias(tunnelInterfaceAlias)
, m_potentialEndpoints(potentialEndpoints)
+ , m_exitEndpointIp(exitEndpointIp)
{
}
@@ -86,8 +92,125 @@ bool PermitVpnTunnel::AddEndpointFilter(const std::optional<Endpoint> &endpoint,
}
+bool PermitVpnTunnel::BlockNonRelayClientExit(const wfp::IpAddress &exitIp, IObjectInstaller &objectInstaller)
+{
+ if (m_relayClients.empty())
+ {
+ // If "relay clients" is empty, then permit connections to exit from any process
+ return true;
+ }
+
+ wfp::FilterBuilder filterBuilder;
+
+ //
+ // Permit traffic to exit relay from relay clients
+ //
+
+ filterBuilder
+ .description(L"This filter is part of a rule that allows exit IP traffic from select clients")
+ .name(L"Permit inbound exit relay connections on tunnel interface")
+ .provider(MullvadGuids::Provider())
+ .sublayer(MullvadGuids::SublayerBaseline())
+ .key(MullvadGuids::Filter_Baseline_PermitVpnTunnel_ExitIp())
+ .weight(wfp::FilterBuilder::WeightClass::Max)
+ .permit();
+
+ if (exitIp.type() == wfp::IpAddress::Ipv4)
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_CONNECT_V4);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_CONNECT_V4);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ for (auto relayClient : m_relayClients) {
+ conditionBuilder.add_condition(std::make_unique<ConditionApplication>(relayClient));
+ }
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_CONNECT_V6);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_CONNECT_V6);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ for (auto relayClient : m_relayClients) {
+ conditionBuilder.add_condition(std::make_unique<ConditionApplication>(relayClient));
+ }
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+
+ //
+ // Block all remaining traffic to the exit
+ //
+
+ {
+ wfp::FilterBuilder filterBuilder;
+
+ filterBuilder
+ .description(L"This filter is part of a rule that blocks exit IP traffic from unexpected clients")
+ .name(L"Block inbound exit relay connections on tunnel interface")
+ .provider(MullvadGuids::Provider())
+ .sublayer(MullvadGuids::SublayerBaseline())
+ .key(MullvadGuids::Filter_Baseline_PermitVpnTunnel_BlockExitIp())
+ .weight(wfp::FilterBuilder::WeightClass::Max)
+ .block();
+
+ if (exitIp.type() == wfp::IpAddress::Ipv4)
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_CONNECT_V4);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_CONNECT_V4);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_CONNECT_V6);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_CONNECT_V6);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+
bool PermitVpnTunnel::apply(IObjectInstaller &objectInstaller)
{
+ if (m_exitEndpointIp.has_value())
+ {
+ if (!BlockNonRelayClientExit(m_exitEndpointIp.value(), objectInstaller))
+ {
+ return false;
+ }
+ }
if (!m_potentialEndpoints.has_value())
{
return AddEndpointFilter(
@@ -98,15 +221,15 @@ bool PermitVpnTunnel::apply(IObjectInstaller &objectInstaller)
);
}
AddEndpointFilter(
- std::make_optional<Endpoint>(m_potentialEndpoints.value().entryEndpoint),
+ std::make_optional<Endpoint>(m_potentialEndpoints.value().endpoint1),
MullvadGuids::Filter_Baseline_PermitVpnTunnel_Outbound_Ipv4_1(),
MullvadGuids::Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_1(),
objectInstaller
);
- if (m_potentialEndpoints.value().exitEndpoint.has_value())
+ if (m_potentialEndpoints.value().endpoint2.has_value())
{
AddEndpointFilter(
- m_potentialEndpoints.value().exitEndpoint.value(),
+ m_potentialEndpoints.value().endpoint2.value(),
MullvadGuids::Filter_Baseline_PermitVpnTunnel_Outbound_Ipv4_2(),
MullvadGuids::Filter_Baseline_PermitVpnTunnel_Outbound_Ipv6_2(),
objectInstaller
diff --git a/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.h b/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.h
index a89ac46d98..e8446f3e35 100644
--- a/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.h
+++ b/windows/winfw/src/winfw/rules/baseline/permitvpntunnel.h
@@ -20,22 +20,27 @@ public:
};
struct Endpoints {
- Endpoint entryEndpoint;
- std::optional<Endpoint> exitEndpoint;
+ Endpoint endpoint1;
+ std::optional<Endpoint> endpoint2;
};
PermitVpnTunnel(
+ const std::vector<std::wstring> &relayClients,
const std::wstring &tunnelInterfaceAlias,
- const std::optional<Endpoints> &potentialEndpoints
+ const std::optional<Endpoints> &potentialEndpoints,
+ const std::optional<wfp::IpAddress> &exitEndpoint
);
bool apply(IObjectInstaller &objectInstaller) override;
private:
bool AddEndpointFilter(const std::optional<Endpoint> &endpoint, const GUID &ipv4Guid, const GUID &ipv6Guid, IObjectInstaller &objectInstaller);
+ bool BlockNonRelayClientExit(const wfp::IpAddress &exitIp, IObjectInstaller &objectInstaller);
+ const std::vector<std::wstring> m_relayClients;
const std::wstring m_tunnelInterfaceAlias;
const std::optional<Endpoints> m_potentialEndpoints;
+ const std::optional<wfp::IpAddress> m_exitEndpointIp;
};
}
diff --git a/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.cpp b/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.cpp
index e5b7ab1f5c..b185dccbd2 100644
--- a/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.cpp
+++ b/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.cpp
@@ -4,6 +4,8 @@
#include <winfw/rules/shared.h>
#include <libwfp/filterbuilder.h>
#include <libwfp/conditionbuilder.h>
+#include <libwfp/conditions/comparison.h>
+#include <libwfp/conditions/conditionapplication.h>
#include <libwfp/conditions/conditioninterface.h>
#include <libwfp/conditions/conditionip.h>
#include <libwfp/conditions/conditionport.h>
@@ -17,11 +19,15 @@ namespace rules::baseline
using Endpoint = PermitVpnTunnel::Endpoint;
PermitVpnTunnelService::PermitVpnTunnelService(
+ const std::vector<std::wstring> &relayClients,
const std::wstring &tunnelInterfaceAlias,
- const std::optional<PermitVpnTunnel::Endpoints> &potentialEndpoints
+ const std::optional<PermitVpnTunnel::Endpoints> &potentialEndpoints,
+ const std::optional<wfp::IpAddress> &exitEndpointIp
)
- : m_tunnelInterfaceAlias(tunnelInterfaceAlias)
+ : m_relayClients(relayClients)
+ , m_tunnelInterfaceAlias(tunnelInterfaceAlias)
, m_potentialEndpoints(potentialEndpoints)
+ , m_exitEndpointIp(exitEndpointIp)
{
}
@@ -88,8 +94,124 @@ bool PermitVpnTunnelService::AddEndpointFilter(const std::optional<PermitVpnTunn
return true;
}
+bool PermitVpnTunnelService::BlockNonRelayClientExit(const wfp::IpAddress &exitIp, IObjectInstaller &objectInstaller)
+{
+ if (m_relayClients.empty())
+ {
+ // If "relay clients" is empty, then permit connections to exit from any process
+ return true;
+ }
+
+ wfp::FilterBuilder filterBuilder;
+
+ //
+ // Permit traffic to exit relay from relay clients
+ //
+
+ filterBuilder
+ .description(L"This filter is part of a rule that allows exit IP traffic from select clients")
+ .name(L"Permit inbound exit relay connections on tunnel interface")
+ .provider(MullvadGuids::Provider())
+ .sublayer(MullvadGuids::SublayerBaseline())
+ .key(MullvadGuids::Filter_Baseline_PermitVpnTunnelService_ExitIp())
+ .weight(wfp::FilterBuilder::WeightClass::Max)
+ .permit();
+
+ if (exitIp.type() == wfp::IpAddress::Ipv4)
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ for (auto relayClient : m_relayClients) {
+ conditionBuilder.add_condition(std::make_unique<ConditionApplication>(relayClient));
+ }
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ for (auto relayClient : m_relayClients) {
+ conditionBuilder.add_condition(std::make_unique<ConditionApplication>(relayClient));
+ }
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+
+ //
+ // Block all remaining traffic to the exit
+ //
+
+ {
+ wfp::FilterBuilder filterBuilder;
+
+ filterBuilder
+ .description(L"This filter is part of a rule that blocks exit IP traffic from unexpected clients")
+ .name(L"Block inbound exit relay connections on tunnel interface")
+ .provider(MullvadGuids::Provider())
+ .sublayer(MullvadGuids::SublayerBaseline())
+ .key(MullvadGuids::Filter_Baseline_PermitVpnTunnelService_BlockExitIp())
+ .weight(wfp::FilterBuilder::WeightClass::Max)
+ .block();
+
+ if (exitIp.type() == wfp::IpAddress::Ipv4)
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ filterBuilder.layer(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6);
+
+ wfp::ConditionBuilder conditionBuilder(FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6);
+
+ conditionBuilder.add_condition(ConditionInterface::Alias(m_tunnelInterfaceAlias));
+ conditionBuilder.add_condition(ConditionIp::Remote(exitIp));
+
+ if (!objectInstaller.addFilter(filterBuilder, conditionBuilder))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
bool PermitVpnTunnelService::apply(IObjectInstaller &objectInstaller)
{
+ if (m_exitEndpointIp.has_value())
+ {
+ if (!BlockNonRelayClientExit(m_exitEndpointIp.value(), objectInstaller))
+ {
+ return false;
+ }
+ }
if (!m_potentialEndpoints.has_value())
{
return AddEndpointFilter(
@@ -100,15 +222,15 @@ bool PermitVpnTunnelService::apply(IObjectInstaller &objectInstaller)
);
}
AddEndpointFilter(
- std::make_optional<Endpoint>(m_potentialEndpoints.value().entryEndpoint),
+ std::make_optional<Endpoint>(m_potentialEndpoints.value().endpoint1),
MullvadGuids::Filter_Baseline_PermitVpnTunnelService_Ipv4_1(),
MullvadGuids::Filter_Baseline_PermitVpnTunnelService_Ipv6_1(),
objectInstaller
);
- if (m_potentialEndpoints.value().exitEndpoint.has_value())
+ if (m_potentialEndpoints.value().endpoint2.has_value())
{
AddEndpointFilter(
- m_potentialEndpoints.value().exitEndpoint.value(),
+ m_potentialEndpoints.value().endpoint2.value(),
MullvadGuids::Filter_Baseline_PermitVpnTunnelService_Ipv4_2(),
MullvadGuids::Filter_Baseline_PermitVpnTunnelService_Ipv6_2(),
objectInstaller
diff --git a/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.h b/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.h
index 8b21dc2264..aa2f805c62 100644
--- a/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.h
+++ b/windows/winfw/src/winfw/rules/baseline/permitvpntunnelservice.h
@@ -15,17 +15,22 @@ class PermitVpnTunnelService : public IFirewallRule
public:
PermitVpnTunnelService(
+ const std::vector<std::wstring> &relayClients,
const std::wstring &tunnelInterfaceAlias,
- const std::optional<PermitVpnTunnel::Endpoints> &potentialEndpoints
+ const std::optional<PermitVpnTunnel::Endpoints> &potentialEndpoints,
+ const std::optional<wfp::IpAddress> &exitEndpointIp
);
bool apply(IObjectInstaller &objectInstaller) override;
private:
bool AddEndpointFilter(const std::optional<PermitVpnTunnel::Endpoint> &endpoint, const GUID &ipv4Guid, const GUID &ipv6Guid, IObjectInstaller &objectInstaller);
+ bool BlockNonRelayClientExit(const wfp::IpAddress &exitIp, IObjectInstaller &objectInstaller);
+ const std::vector<std::wstring> m_relayClients;
const std::wstring m_tunnelInterfaceAlias;
const std::optional<PermitVpnTunnel::Endpoints> m_potentialEndpoints;
+ const std::optional<wfp::IpAddress> m_exitEndpointIp;
};
}
diff --git a/windows/winfw/src/winfw/winfw.cpp b/windows/winfw/src/winfw/winfw.cpp
index 0ed0fe5731..d962965a19 100644
--- a/windows/winfw/src/winfw/winfw.cpp
+++ b/windows/winfw/src/winfw/winfw.cpp
@@ -240,6 +240,7 @@ WINFW_API
WinFw_ApplyPolicyConnecting(
const WinFwSettings *settings,
const WinFwEndpoint *relay,
+ const wchar_t *exitEndpointIp,
const wchar_t **relayClients,
size_t relayClientsLen,
const wchar_t *tunnelInterfaceAlias,
@@ -269,6 +270,14 @@ WinFw_ApplyPolicyConnecting(
THROW_ERROR("Invalid argument: allowedTunnelTraffic");
}
+ const auto exitIpAddr = (exitEndpointIp != nullptr) ? std::make_optional(wfp::IpAddress(exitEndpointIp)) : std::nullopt;
+ const auto entryIpAddr = wfp::IpAddress(relay->ip);
+
+ if (entryIpAddr == exitIpAddr)
+ {
+ THROW_ERROR("Invalid argument: relay IP must not equal exitEndpointIp");
+ }
+
std::vector<std::wstring> relayClientWstrings;
relayClientWstrings.reserve(relayClientsLen);
for(int i = 0; i < relayClientsLen; i++) {
@@ -278,6 +287,7 @@ WinFw_ApplyPolicyConnecting(
return g_fwContext->applyPolicyConnecting(
*settings,
*relay,
+ exitIpAddr,
relayClientWstrings,
tunnelInterfaceAlias != nullptr ? std::make_optional(tunnelInterfaceAlias) : std::nullopt,
MakeOptional(allowedEndpoint),
@@ -309,6 +319,7 @@ WINFW_API
WinFw_ApplyPolicyConnected(
const WinFwSettings *settings,
const WinFwEndpoint *relay,
+ const wchar_t *exitEndpointIp,
const wchar_t **relayClients,
size_t relayClientsLen,
const wchar_t *tunnelInterfaceAlias,
@@ -350,6 +361,14 @@ WinFw_ApplyPolicyConnected(
THROW_ERROR("Invalid argument: nonTunnelDnsServers");
}
+ const auto exitIpAddr = (exitEndpointIp != nullptr) ? std::make_optional(wfp::IpAddress(exitEndpointIp)) : std::nullopt;
+ const auto entryIpAddr = wfp::IpAddress(relay->ip);
+
+ if (entryIpAddr == exitIpAddr)
+ {
+ THROW_ERROR("Invalid argument: relay IP must not equal exitEndpointIp");
+ }
+
std::vector<wfp::IpAddress> convertedTunnelDnsServers;
std::vector<wfp::IpAddress> convertedNonTunnelDnsServers;
@@ -398,6 +417,7 @@ WinFw_ApplyPolicyConnected(
return g_fwContext->applyPolicyConnected(
*settings,
*relay,
+ exitIpAddr,
relayClientWstrings,
tunnelInterfaceAlias,
convertedTunnelDnsServers,
diff --git a/windows/winfw/src/winfw/winfw.h b/windows/winfw/src/winfw/winfw.h
index f40d835a95..f3e26ea4aa 100644
--- a/windows/winfw/src/winfw/winfw.h
+++ b/windows/winfw/src/winfw/winfw.h
@@ -71,6 +71,7 @@ typedef struct tag_WinFwAllowedTunnelTraffic
}
WinFwAllowedTunnelTraffic;
+
///////////////////////////////////////////////////////////////////////////////
// Functions
///////////////////////////////////////////////////////////////////////////////
@@ -161,6 +162,12 @@ enum WINFW_POLICY_STATUS
// - Communication with the relay server
// - Specified in-tunnel traffic, except DNS.
//
+// Parameters:
+//
+// exitEndpointIp:
+// IP address of the exit relay, if it differs from `relay`. Otherwise, this should be set to
+// `nullptr`.
+//
extern "C"
WINFW_LINKAGE
WINFW_POLICY_STATUS
@@ -168,6 +175,7 @@ WINFW_API
WinFw_ApplyPolicyConnecting(
const WinFwSettings *settings,
const WinFwEndpoint *relay,
+ const wchar_t *exitEndpointIp,
const wchar_t **relayClient,
size_t relayClientLen,
const wchar_t *tunnelInterfaceAlias,
@@ -190,6 +198,10 @@ WinFw_ApplyPolicyConnecting(
// tunnelInterfaceAlias:
// Friendly name of VPN tunnel interface
//
+// exitEndpointIp:
+// IP address of the exit relay, if it differs from `relay`. Otherwise, this should be set to
+// `nullptr`.
+//
extern "C"
WINFW_LINKAGE
WINFW_POLICY_STATUS
@@ -197,6 +209,7 @@ WINFW_API
WinFw_ApplyPolicyConnected(
const WinFwSettings *settings,
const WinFwEndpoint *relay,
+ const wchar_t *exitEndpointIp,
const wchar_t **relayClient,
size_t relayClientLen,
const wchar_t *tunnelInterfaceAlias,