summaryrefslogtreecommitdiffhomepage
path: root/talpid-core/src
diff options
context:
space:
mode:
authorLinus Färnstrand <linus@mullvad.net>2024-11-06 16:23:07 +0100
committerLinus Färnstrand <linus@mullvad.net>2024-11-08 09:39:54 +0100
commit1d0bcb0a4665014c73c310e98e16ea4d67e2a6ec (patch)
tree919705f05247813e1e4ea675847b6f33fb2e045b /talpid-core/src
parentdfb7d99822969b44e6dcad0e896971cb3ec58231 (diff)
downloadmullvadvpn-1d0bcb0a4665014c73c310e98e16ea4d67e2a6ec.tar.xz
mullvadvpn-1d0bcb0a4665014c73c310e98e16ea4d67e2a6ec.zip
Set net.ipv4.conf.all.arp_ignore=2 when bringing up tunnels on Linux
Prevent attackers able to send ARP requests to the device running Mullvad from figuring out the in-tunnel IP. Fixes 2024 audit issue `MLLVD-CR-24-03`.
Diffstat (limited to 'talpid-core/src')
-rw-r--r--talpid-core/src/firewall/linux.rs44
1 files changed, 40 insertions, 4 deletions
diff --git a/talpid-core/src/firewall/linux.rs b/talpid-core/src/firewall/linux.rs
index 71a13ad914..26d3a5cb5a 100644
--- a/talpid-core/src/firewall/linux.rs
+++ b/talpid-core/src/firewall/linux.rs
@@ -21,6 +21,7 @@ use talpid_types::net::{
const MANGLE_CHAIN_PRIORITY: i32 = libc::NF_IP_PRI_MANGLE;
const PREROUTING_CHAIN_PRIORITY: i32 = libc::NF_IP_PRI_CONNTRACK + 1;
const PROC_SYS_NET_IPV4_CONF_SRC_VALID_MARK: &str = "/proc/sys/net/ipv4/conf/all/src_valid_mark";
+const PROC_SYS_NET_IPV4_CONF_ARP_IGNORE: &str = "/proc/sys/net/ipv4/conf/all/arp_ignore";
pub type Result<T> = std::result::Result<T, Error>;
@@ -76,6 +77,11 @@ static DONT_SET_SRC_VALID_MARK: LazyLock<bool> = LazyLock::new(|| {
.map(|v| v != "0")
.unwrap_or(false)
});
+static DONT_SET_ARP_IGNORE: LazyLock<bool> = LazyLock::new(|| {
+ env::var("TALPID_FIREWALL_DONT_SET_ARP_IGNORE")
+ .map(|v| v != "0")
+ .unwrap_or(false)
+});
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum Direction {
@@ -134,14 +140,29 @@ impl Firewall {
fn apply_kernel_config(policy: &FirewallPolicy) {
if *DONT_SET_SRC_VALID_MARK {
log::debug!("Not setting src_valid_mark");
- return;
- }
-
- if let FirewallPolicy::Connecting { .. } = policy {
+ } else if let FirewallPolicy::Connecting { .. } = policy {
if let Err(err) = set_src_valid_mark_sysctl() {
log::error!("Failed to apply src_valid_mark: {}", err);
}
}
+
+ // When we have a tunnel with an IP configured, we configure the system
+ // to not reply to arp requests for this tunnel IP *on other interfaces*.
+ // By default, Linux responds to incoming arp requests for any IP configured on any
+ // interface on the system. This makes it possible to via ARP-pinging figure out the
+ // VPN in-tunnel IP by spamming ARP requests to any physical interface on the device.
+ //
+ // We never store the initial value and restore it. We deem the default value
+ // to be too relaxed and don't see any reason why anyone would want a more relaxed
+ // setting than the one we are setting here.
+ if *DONT_SET_ARP_IGNORE {
+ log::debug!("Not setting arp_ignore");
+ } else if let FirewallPolicy::Connecting { .. } | FirewallPolicy::Connected { .. } = policy
+ {
+ if let Err(err) = lock_down_arp_ignore_sysctl() {
+ log::error!("Failed to apply arp_ignore: {}", err);
+ }
+ }
}
fn send_and_process(batch: &FinalizedBatch) -> Result<()> {
@@ -1054,6 +1075,21 @@ fn set_src_valid_mark_sysctl() -> io::Result<()> {
fs::write(PROC_SYS_NET_IPV4_CONF_SRC_VALID_MARK, b"1")
}
+/// If the `net.ipv4.conf.all.arp_ignore` setting is below 2, sets it to 2.
+///
+/// 2 means: reply only if the target IP address is local address configured on the incoming
+/// interface and both with the sender's IP address are part from same subnet on this interface.
+fn lock_down_arp_ignore_sysctl() -> io::Result<()> {
+ // Should be safe to treat the content as a string, since it should always be a number.
+ let current_arp_ignore = fs::read_to_string(PROC_SYS_NET_IPV4_CONF_ARP_IGNORE)?;
+ match current_arp_ignore.trim() {
+ "0" | "1" => fs::write(PROC_SYS_NET_IPV4_CONF_ARP_IGNORE, b"2")?,
+ "2" => (),
+ _ => log::trace!("Not locking down arp_ignore since it is set to {current_arp_ignore}"),
+ }
+ Ok(())
+}
+
/// Tables that are no longer used but need to be deleted due to upgrades.
/// This can be removed when upgrades from 2023.3 are no longer supported.
fn batch_deprecated_tables(batch: &mut Batch) {