diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs index 64c65d2c4eac..bc178a6c6458 100644 --- a/talpid-wireguard/src/lib.rs +++ b/talpid-wireguard/src/lib.rs @@ -165,12 +165,16 @@ impl WireguardMonitor { let endpoint_addrs: Vec = config.peers().map(|peer| peer.endpoint.ip()).collect(); let (close_obfs_sender, close_obfs_listener) = sync_mpsc::channel(); + // Start obfuscation server and patch the WireGuard config to point the endpoint to it. let obfuscator = args .runtime .block_on(obfuscation::apply_obfuscation_config( &mut config, close_obfs_sender.clone(), ))?; + if let Some(obfuscator) = obfuscator.as_ref() { + config.mtu = config.mtu.saturating_sub(obfuscator.packet_overhead()); + } #[cfg(target_os = "windows")] let (setup_done_tx, setup_done_rx) = mpsc::channel(0); @@ -374,7 +378,7 @@ impl WireguardMonitor { args: TunnelArgs<'_, F>, ) -> Result { let (close_obfs_sender, close_obfs_listener) = sync_mpsc::channel(); - // TODO: Document the side effect of starting this before opening the tunnel! + // Start obfuscation server and patch the WireGuard config to point the endpoint to it. let obfuscator = args .runtime .block_on(obfuscation::apply_obfuscation_config( @@ -382,6 +386,9 @@ impl WireguardMonitor { close_obfs_sender.clone(), args.tun_provider.clone(), ))?; + if let Some(obfuscator) = obfuscator.as_ref() { + config.mtu = config.mtu.saturating_sub(obfuscator.packet_overhead()); + } let should_negotiate_ephemeral_peer = config.quantum_resistant || config.daita; let tunnel = Self::open_tunnel( diff --git a/talpid-wireguard/src/obfuscation.rs b/talpid-wireguard/src/obfuscation.rs index 0e1e7273e817..7317c4bfaf64 100644 --- a/talpid-wireguard/src/obfuscation.rs +++ b/talpid-wireguard/src/obfuscation.rs @@ -32,29 +32,15 @@ pub async fn apply_obfuscation_config( #[cfg(target_os = "linux")] config.fwmark, ); - apply_obfuscation_config_inner( - config, - settings, - close_msg_sender, - #[cfg(target_os = "android")] - tun_provider, - ) - .await - .map(Some) -} -async fn apply_obfuscation_config_inner( - config: &mut Config, - settings: ObfuscationSettings, - close_msg_sender: sync_mpsc::Sender, - #[cfg(target_os = "android")] tun_provider: Arc>, -) -> Result { log::trace!("Obfuscation settings: {settings:?}"); let obfuscator = create_obfuscator(&settings) .await .map_err(Error::ObfuscationError)?; + let packet_overhead = obfuscator.packet_overhead(); + #[cfg(target_os = "android")] bypass_vpn(tun_provider, obfuscator.remote_socket_fd()).await; @@ -76,7 +62,10 @@ async fn apply_obfuscation_config_inner( } }); - Ok(ObfuscatorHandle::new(obfuscation_task)) + Ok(Some(ObfuscatorHandle { + obfuscation_task, + packet_overhead, + })) } /// Patch the first peer in the WireGuard configuration to use the local proxy endpoint @@ -129,16 +118,17 @@ async fn bypass_vpn( /// Simple wrapper that automatically cancels the future which runs an obfuscator. pub struct ObfuscatorHandle { obfuscation_task: tokio::task::JoinHandle<()>, + packet_overhead: u16, } impl ObfuscatorHandle { - pub fn new(obfuscation_task: tokio::task::JoinHandle<()>) -> Self { - Self { obfuscation_task } - } - pub fn abort(&self) { self.obfuscation_task.abort(); } + + pub fn packet_overhead(&self) -> u16 { + self.packet_overhead + } } impl Drop for ObfuscatorHandle { diff --git a/tunnel-obfuscation/src/lib.rs b/tunnel-obfuscation/src/lib.rs index 62528ed609af..9b4ca08d4859 100644 --- a/tunnel-obfuscation/src/lib.rs +++ b/tunnel-obfuscation/src/lib.rs @@ -31,6 +31,11 @@ pub trait Obfuscator: Send { /// Returns the file descriptor of the outbound socket. #[cfg(target_os = "android")] fn remote_socket_fd(&self) -> std::os::unix::io::RawFd; + + /// The overhead (in bytes) of this obfuscation protocol. + /// + /// This is used when deciding on MTUs. + fn packet_overhead(&self) -> u16; } #[derive(Debug)] diff --git a/tunnel-obfuscation/src/shadowsocks.rs b/tunnel-obfuscation/src/shadowsocks.rs index c529f65a19ec..87622506f591 100644 --- a/tunnel-obfuscation/src/shadowsocks.rs +++ b/tunnel-obfuscation/src/shadowsocks.rs @@ -58,6 +58,7 @@ pub enum Error { pub struct Shadowsocks { udp_client_addr: SocketAddr, + wireguard_endpoint: SocketAddr, server: tokio::task::JoinHandle>, // The receiver will implicitly shut down when this is dropped _shutdown_tx: oneshot::Sender<()>, @@ -101,6 +102,7 @@ impl Shadowsocks { Ok(Shadowsocks { udp_client_addr, + wireguard_endpoint: settings.wireguard_endpoint, server, _shutdown_tx: shutdown_tx, #[cfg(target_os = "android")] @@ -285,6 +287,19 @@ impl Obfuscator for Shadowsocks { fn remote_socket_fd(&self) -> std::os::unix::io::RawFd { self.outbound_fd } + + fn packet_overhead(&self) -> u16 { + // This math relies on the packet structure of Shadowsocks AEAD UDP packets. + // https://shadowsocks.org/doc/aead.html + // Those packets look like this: [salt][address][payload][tag] + debug_assert!(SHADOWSOCKS_CIPHER.is_aead()); + + let overhead = SHADOWSOCKS_CIPHER.salt_len() + + Address::from(self.wireguard_endpoint).serialized_len() + + SHADOWSOCKS_CIPHER.tag_len(); + + u16::try_from(overhead).expect("packet overhead is less than u16::MAX") + } } /// Return whether retrying is a lost cause diff --git a/tunnel-obfuscation/src/udp2tcp.rs b/tunnel-obfuscation/src/udp2tcp.rs index 450d8d3bba2a..6532e0624b81 100644 --- a/tunnel-obfuscation/src/udp2tcp.rs +++ b/tunnel-obfuscation/src/udp2tcp.rs @@ -85,4 +85,16 @@ impl Obfuscator for Udp2Tcp { fn remote_socket_fd(&self) -> std::os::unix::io::RawFd { self.instance.remote_tcp_fd() } + + fn packet_overhead(&self) -> u16 { + let max_tcp_header_len = 60; // https://datatracker.ietf.org/doc/html/rfc9293#section-3.1-6.22.1 + let udp_header_len = 8; // https://datatracker.ietf.org/doc/html/rfc768 + + // TODO: Make `HEADER_LEN` constant public in udp-over-tcp lib and use it instead + let udp_over_tcp_header_len = size_of::(); + + let overhead = max_tcp_header_len - udp_header_len + udp_over_tcp_header_len; + + u16::try_from(overhead).expect("packet overhead is less than u16::MAX") + } }