summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2023-04-17 16:37:05 +0200
committerDavid Lönnhager <david.l@mullvad.net>2023-04-17 16:37:05 +0200
commitfeb170330802fe18c83a577ffb6c2cbce5ccbf96 (patch)
tree1c09bad7b959d9d7c8c6de62eabb78d013545237
parent1ae978475239a1e4cd8c205ddef4ed2fd95bda02 (diff)
parent3b3f24cea05e2805444e2707722909f980c27729 (diff)
downloadmullvadvpn-feb170330802fe18c83a577ffb6c2cbce5ccbf96.tar.xz
mullvadvpn-feb170330802fe18c83a577ffb6c2cbce5ccbf96.zip
Merge branch 'pq-quickfix-mtu' into main
-rw-r--r--CHANGELOG.md3
-rw-r--r--Cargo.lock2
-rw-r--r--talpid-tunnel-config-client/Cargo.toml6
-rw-r--r--talpid-tunnel-config-client/src/lib.rs120
-rw-r--r--talpid-wireguard/src/lib.rs41
5 files changed, 159 insertions, 13 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fe3de44c8..83184591d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,9 @@ Line wrap the file at 100 chars. Th
- Fix duplicated notifications in some situations.
- Fix notification setting being inverted. When non-important notifications were disabled it instead
disabled important ones and showed non-important ones.
+- Work around issues with PQ and multihop caused by fragmentation in the tunnel. The workaround
+ doesn't fix fragmentation issues in general but prevents the PSK exchange packets from being
+ fragmented by setting an explicit maximum segment size.
#### Android
- Fix adaptive app icon which previously had a displaced nose and some other oddities.
diff --git a/Cargo.lock b/Cargo.lock
index ce7e86ffe7..f1d8e294a3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3359,6 +3359,7 @@ name = "talpid-tunnel-config-client"
version = "0.0.0"
dependencies = [
"classic-mceliece-rust",
+ "libc",
"log",
"pqc_kyber",
"prost",
@@ -3368,6 +3369,7 @@ dependencies = [
"tonic",
"tonic-build",
"tower",
+ "windows-sys 0.45.0",
"zeroize",
]
diff --git a/talpid-tunnel-config-client/Cargo.toml b/talpid-tunnel-config-client/Cargo.toml
index 62542da683..0f39c4d1ea 100644
--- a/talpid-tunnel-config-client/Cargo.toml
+++ b/talpid-tunnel-config-client/Cargo.toml
@@ -18,6 +18,12 @@ tokio = "1"
classic-mceliece-rust = { version = "2.0.0", features = ["mceliece460896f", "zeroize"] }
pqc_kyber = { version = "0.4.0", features = ["std", "kyber1024", "zeroize"] }
zeroize = "1.5.7"
+libc = "0.2"
+
+[target.'cfg(target_os="windows")'.dependencies]
+windows-sys = { version = "0.45.0", features = [
+ "Win32_Networking_WinSock"
+]}
[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread"] }
diff --git a/talpid-tunnel-config-client/src/lib.rs b/talpid-tunnel-config-client/src/lib.rs
index b795f6b814..f826eefe4f 100644
--- a/talpid-tunnel-config-client/src/lib.rs
+++ b/talpid-tunnel-config-client/src/lib.rs
@@ -1,6 +1,11 @@
-use std::{fmt, net::IpAddr};
+use std::{
+ fmt,
+ net::{IpAddr, SocketAddr},
+};
use talpid_types::net::wireguard::{PresharedKey, PublicKey};
-use tonic::transport::Channel;
+use tokio::net::TcpSocket;
+use tonic::transport::{Channel, Endpoint};
+use tower::service_fn;
use zeroize::Zeroize;
mod classic_mceliece;
@@ -11,6 +16,20 @@ mod proto {
tonic::include_proto!("tunnel_config");
}
+use libc::setsockopt;
+
+#[cfg(not(target_os = "windows"))]
+mod sys {
+ pub use libc::{socklen_t, IPPROTO_TCP, TCP_MAXSEG};
+ pub use std::os::fd::{AsRawFd, RawFd};
+}
+#[cfg(target_os = "windows")]
+mod sys {
+ pub use std::os::windows::io::{AsRawSocket, RawSocket};
+ pub use windows_sys::Win32::Networking::WinSock::{IPPROTO_IP, IPPROTO_TCP, IP_USER_MTU};
+}
+use sys::*;
+
#[derive(Debug)]
pub enum Error {
GrpcConnectError(tonic::transport::Error),
@@ -72,10 +91,19 @@ pub async fn push_pq_key(
wg_pubkey: PublicKey,
wg_psk_pubkey: PublicKey,
) -> Result<PresharedKey, Error> {
+ push_pq_key_with_opts(service_address, wg_pubkey, wg_psk_pubkey, None).await
+}
+
+pub async fn push_pq_key_with_opts(
+ service_address: IpAddr,
+ wg_pubkey: PublicKey,
+ wg_psk_pubkey: PublicKey,
+ mtu: Option<u16>,
+) -> Result<PresharedKey, Error> {
let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await;
let kyber_keypair = kyber::keypair(&mut rand::thread_rng());
- let mut client = new_client(service_address).await?;
+ let mut client = new_client(service_address, mtu).await?;
let response = client
.psk_exchange_v1(proto::PskRequestV1 {
wg_pubkey: wg_pubkey.as_bytes().to_vec(),
@@ -111,8 +139,9 @@ pub async fn push_pq_key(
let mut shared_secret = classic_mceliece::decapsulate(&cme_kem_secret, cme_ciphertext)?;
xor_assign(&mut psk_data, shared_secret.as_array());
- // This should happen automatically due to `SharedSecret` implementing ZeroizeOnDrop. But doing it explicitly
- // provides a stronger guarantee that it's not accidentally removed.
+ // This should happen automatically due to `SharedSecret` implementing ZeroizeOnDrop. But
+ // doing it explicitly provides a stronger guarantee that it's not accidentally
+ // removed.
shared_secret.zeroize();
}
// Decapsulate Kyber and mix into PSK
@@ -137,8 +166,83 @@ fn xor_assign(dst: &mut [u8; 32], src: &[u8; 32]) {
}
}
-async fn new_client(addr: IpAddr) -> Result<RelayConfigService, Error> {
- RelayConfigService::connect(format!("tcp://{addr}:{CONFIG_SERVICE_PORT}"))
+async fn new_client(addr: IpAddr, mtu: Option<u16>) -> Result<RelayConfigService, Error> {
+ let endpoint = Endpoint::from_static("tcp://0.0.0.0:0");
+
+ let conn = endpoint
+ .connect_with_connector(service_fn(move |_| async move {
+ let sock = TcpSocket::new_v4()?;
+
+ if let Some(mtu) = mtu {
+ #[cfg(target_os = "windows")]
+ try_set_tcp_sock_mtu(sock.as_raw_socket(), mtu);
+
+ #[cfg(not(target_os = "windows"))]
+ try_set_tcp_sock_mtu(&addr, sock.as_raw_fd(), mtu);
+ }
+
+ sock.connect(SocketAddr::new(addr, CONFIG_SERVICE_PORT))
+ .await
+ }))
.await
- .map_err(Error::GrpcConnectError)
+ .map_err(Error::GrpcConnectError)?;
+
+ Ok(RelayConfigService::new(conn))
+}
+
+#[cfg(windows)]
+fn try_set_tcp_sock_mtu(sock: RawSocket, mtu: u16) {
+ let mtu = u32::from(mtu);
+ log::debug!("Config client socket MTU: {mtu}");
+
+ let raw_sock = usize::try_from(sock).unwrap();
+
+ let result = unsafe {
+ setsockopt(
+ raw_sock,
+ IPPROTO_IP as i32,
+ IP_USER_MTU as i32,
+ &mtu as *const _ as _,
+ std::ffi::c_int::try_from(std::mem::size_of_val(&mtu)).unwrap(),
+ )
+ };
+ if result != 0 {
+ log::error!(
+ "Failed to set user MTU on config client socket: {}",
+ std::io::Error::last_os_error()
+ );
+ }
+}
+
+#[cfg(not(windows))]
+fn try_set_tcp_sock_mtu(dest: &IpAddr, sock: RawFd, mut mtu: u16) {
+ const IPV4_HEADER_SIZE: u16 = 20;
+ const IPV6_HEADER_SIZE: u16 = 40;
+ const MAX_TCP_HEADER_SIZE: u16 = 60;
+
+ if dest.is_ipv4() {
+ mtu = mtu.saturating_sub(IPV4_HEADER_SIZE);
+ } else {
+ mtu = mtu.saturating_sub(IPV6_HEADER_SIZE);
+ }
+
+ let mss = u32::from(mtu.saturating_sub(MAX_TCP_HEADER_SIZE));
+
+ log::debug!("Config client socket MSS: {mss}");
+
+ let result = unsafe {
+ setsockopt(
+ sock,
+ IPPROTO_TCP,
+ TCP_MAXSEG,
+ &mss as *const _ as _,
+ socklen_t::try_from(std::mem::size_of_val(&mss)).unwrap(),
+ )
+ };
+ if result != 0 {
+ log::error!(
+ "Failed to set MSS on config client socket: {}",
+ std::io::Error::last_os_error()
+ );
+ }
}
diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs
index 85b4c5a6df..3d542641b7 100644
--- a/talpid-wireguard/src/lib.rs
+++ b/talpid-wireguard/src/lib.rs
@@ -121,8 +121,8 @@ pub struct WireguardMonitor {
obfuscator: Arc<AsyncMutex<Option<ObfuscatorHandle>>>,
}
-const INITIAL_PSK_EXCHANGE_TIMEOUT: Duration = Duration::from_secs(4);
-const MAX_PSK_EXCHANGE_TIMEOUT: Duration = Duration::from_secs(15);
+const INITIAL_PSK_EXCHANGE_TIMEOUT: Duration = Duration::from_secs(8);
+const MAX_PSK_EXCHANGE_TIMEOUT: Duration = Duration::from_secs(48);
const PSK_EXCHANGE_TIMEOUT_MULTIPLIER: u32 = 2;
/// Simple wrapper that automatically cancels the future which runs an obfuscator.
@@ -473,8 +473,9 @@ impl WireguardMonitor {
let metadata = Self::tunnel_metadata(iface_name, config);
(on_event)(TunnelEvent::InterfaceUp(metadata, allowed_traffic.clone())).await;
+ let mtu = Self::get_exit_psk_socket_mtu(config);
let exit_psk =
- Self::perform_psk_negotiation(retry_attempt, config, wg_psk_privkey.public_key())
+ Self::perform_psk_negotiation(retry_attempt, config, wg_psk_privkey.public_key(), mtu)
.await?;
log::debug!("Successfully exchanged PSK with exit peer");
@@ -507,6 +508,7 @@ impl WireguardMonitor {
retry_attempt,
&entry_config,
wg_psk_privkey.public_key(),
+ None,
)
.await?,
);
@@ -638,6 +640,7 @@ impl WireguardMonitor {
retry_attempt: u32,
config: &Config,
wg_psk_pubkey: PublicKey,
+ mss: Option<u16>,
) -> std::result::Result<PresharedKey, CloseMsg> {
log::debug!("Performing PQ-safe PSK exchange");
@@ -649,10 +652,11 @@ impl WireguardMonitor {
let psk = tokio::time::timeout(
timeout,
- talpid_tunnel_config_client::push_pq_key(
- IpAddr::V4(config.ipv4_gateway),
+ talpid_tunnel_config_client::push_pq_key_with_opts(
+ Self::get_tunnel_config_client_addr(config),
config.tunnel.private_key.public_key(),
wg_psk_pubkey,
+ mss,
),
)
.await
@@ -666,6 +670,33 @@ impl WireguardMonitor {
Ok(psk)
}
+ /// TODO: This hack makes sure that the max segment size is small enough to prevent
+ /// fragmentation. This is an issue when using multihop, because the MTU is too small to
+ /// accommodate the headers of the inner tunnel, and lowering it does no good.
+ //
+ /// When we have fixed fragmentation issues for multihop, this workaround can be removed.
+ ///
+ /// Note that TCP imposes max and min bounds on the specified size as well.
+ fn get_exit_psk_socket_mtu(config: &Config) -> Option<u16> {
+ // macOS is averse(?) to large values for MSS, so we just use the minimum
+ const MIN_IPV4_MTU: u16 = 576;
+ const MIN_IPV6_MTU: u16 = 1280;
+
+ if config.peers.len() == 1 {
+ return None;
+ }
+
+ if Self::get_tunnel_config_client_addr(config).is_ipv4() {
+ Some(MIN_IPV4_MTU)
+ } else {
+ Some(MIN_IPV6_MTU)
+ }
+ }
+
+ fn get_tunnel_config_client_addr(config: &Config) -> IpAddr {
+ IpAddr::V4(config.ipv4_gateway)
+ }
+
#[allow(unused_variables)]
fn open_tunnel(
runtime: tokio::runtime::Handle,