diff options
| author | Linus Färnstrand <linus@mullvad.net> | 2024-11-06 16:23:07 +0100 |
|---|---|---|
| committer | Linus Färnstrand <linus@mullvad.net> | 2024-11-08 09:39:54 +0100 |
| commit | 1d0bcb0a4665014c73c310e98e16ea4d67e2a6ec (patch) | |
| tree | 919705f05247813e1e4ea675847b6f33fb2e045b /talpid-core/src | |
| parent | dfb7d99822969b44e6dcad0e896971cb3ec58231 (diff) | |
| download | mullvadvpn-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.rs | 44 |
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) { |
