diff options
| author | David Lönnhager <david.l@mullvad.net> | 2024-11-19 10:25:44 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2024-11-22 13:38:16 +0100 |
| commit | f4db85b3a552f60d2454bfa69912c7ced51b41b1 (patch) | |
| tree | c932bc8d75ea3ca6d95dfdd0c3925a171cea9d07 /talpid-core/src | |
| parent | 8ababf0f77b23f7245a1aed3d8c8c4a5e3c06192 (diff) | |
| download | mullvadvpn-f4db85b3a552f60d2454bfa69912c7ced51b41b1.tar.xz mullvadvpn-f4db85b3a552f60d2454bfa69912c7ced51b41b1.zip | |
Add non-blocking DNS resolver for Android API requests
Diffstat (limited to 'talpid-core/src')
| -rw-r--r-- | talpid-core/src/connectivity_listener.rs | 249 | ||||
| -rw-r--r-- | talpid-core/src/lib.rs | 4 | ||||
| -rw-r--r-- | talpid-core/src/offline/android.rs | 211 | ||||
| -rw-r--r-- | talpid-core/src/offline/mod.rs | 8 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/mod.rs | 13 |
5 files changed, 279 insertions, 206 deletions
diff --git a/talpid-core/src/connectivity_listener.rs b/talpid-core/src/connectivity_listener.rs new file mode 100644 index 0000000000..b28f58cefb --- /dev/null +++ b/talpid-core/src/connectivity_listener.rs @@ -0,0 +1,249 @@ +//! Rust wrapper around Android connectivity listener + +use futures::channel::mpsc::UnboundedSender; +use jnix::{ + jni::{ + self, + objects::{GlobalRef, JObject, JValue}, + signature::{JavaType, Primitive}, + sys::{jboolean, jlong, JNI_TRUE}, + JNIEnv, JavaVM, + }, + JnixEnv, + FromJava, +}; +use std::{net::IpAddr, sync::{Arc, Weak}}; +use talpid_types::{android::AndroidContext, net::Connectivity, ErrorExt}; + +/// Error related to Android connectivity monitor +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Failed to attach Java VM to tunnel thread + #[error("Failed to attach Java VM to tunnel thread")] + AttachJvmToThread(#[source] jni::errors::Error), + + /// Failed to call Java method + #[error("Failed to call Java method {0}.{1}")] + CallMethod(&'static str, &'static str, #[source] jni::errors::Error), + + /// Failed to create global reference to Java object + #[error("Failed to create global reference to Java object")] + CreateGlobalRef(#[source] jni::errors::Error), + + /// Failed to find method + #[error("Failed to find {0}.{1} method")] + FindMethod(&'static str, &'static str, #[source] jni::errors::Error), + + /// Method returned invalid result + #[error("Received an invalid result from {0}.{1}: {2}")] + InvalidMethodResult(&'static str, &'static str, String), +} + +/// Android connectivity listener +#[derive(Clone)] +pub struct ConnectivityListener { + jvm: Arc<JavaVM>, + class: GlobalRef, + object: GlobalRef, + _sender: Option<Arc<UnboundedSender<Connectivity>>>, +} + +impl ConnectivityListener { + /// Create a new connectivity listener + pub fn new(android_context: AndroidContext) -> Result<Self, Error> { + let env = JnixEnv::from( + android_context + .jvm + .attach_current_thread_as_daemon() + .map_err(Error::AttachJvmToThread)?, + ); + + let get_connectivity_listener_method = env + .get_method_id( + &env.get_class("net/mullvad/talpid/TalpidVpnService"), + "getConnectivityListener", + "()Lnet/mullvad/talpid/ConnectivityListener;", + ) + .map_err(|cause| { + Error::FindMethod("MullvadVpnService", "getConnectivityListener", cause) + })?; + + let result = env + .call_method_unchecked( + android_context.vpn_service.as_obj(), + get_connectivity_listener_method, + JavaType::Object("Lnet/mullvad/talpid/ConnectivityListener;".to_owned()), + &[], + ) + .map_err(|cause| { + Error::CallMethod("MullvadVpnService", "getConnectivityListener", cause) + })?; + + let object = match result { + JValue::Object(object) => env.new_global_ref(object).map_err(Error::CreateGlobalRef)?, + value => { + return Err(Error::InvalidMethodResult( + "MullvadVpnService", + "getConnectivityListener", + format!("{:?}", value), + )) + } + }; + + let class = env.get_class("net/mullvad/talpid/ConnectivityListener"); + + Ok(ConnectivityListener { + jvm: android_context.jvm, + class, + object, + _sender: None, + }) + } + + /// Register a channel that receives changes about the offline state + pub fn set_connectivity_listener( + &mut self, + sender: UnboundedSender<Connectivity>, + ) -> Result<(), Error> { + let sender = Arc::new(sender); + + let weak_sender = Arc::downgrade(&sender); + + let weak_sender_ptr = Box::new(weak_sender); + let weak_sender_address = Box::into_raw(weak_sender_ptr) as jlong; + + let result = self.call_method( + "setSenderAddress", + "(J)V", + &[JValue::Long(weak_sender_address)], + JavaType::Primitive(Primitive::Void), + )?; + + match result { + JValue::Void => Ok(()), + value => Err(Error::InvalidMethodResult( + "ConnectivityListener", + "setSenderAddress", + format!("{:?}", value), + )), + }?; + + self._sender = Some(sender); + + Ok(()) + } + + /// Return the current offline/connectivity state + pub fn connectivity(&self) -> Connectivity { + self.get_is_connected() + .map(|connected| Connectivity::Status { connected }) + .unwrap_or_else(|error| { + log::error!( + "{}", + error.display_chain_with_msg("Failed to check connectivity status") + ); + Connectivity::PresumeOnline + }) + } + + fn get_is_connected(&self) -> Result<bool, Error> { + let is_connected = self.call_method( + "isConnected", + "()Z", + &[], + JavaType::Primitive(Primitive::Boolean), + )?; + + match is_connected { + JValue::Bool(JNI_TRUE) => Ok(true), + JValue::Bool(_) => Ok(false), + value => Err(Error::InvalidMethodResult( + "ConnectivityListener", + "isConnected", + format!("{:?}", value), + )), + } + } + + /// Return the current DNS servers according to Android + pub fn current_dns_servers(&self) -> Result<Vec<IpAddr>, Error> { + let env = JnixEnv::from( + self.jvm + .attach_current_thread_as_daemon() + .map_err(Error::AttachJvmToThread)?, + ); + + let current_dns_servers = self.call_method( + "getCurrentDnsServers", + "()Ljava/util/ArrayList;", + &[], + JavaType::Object("java/util/ArrayList".to_owned()), + )?; + + match current_dns_servers { + JValue::Object(jaddrs) => Ok(Vec::from_java(&env, jaddrs)), + value => Err(Error::InvalidMethodResult( + "ConnectivityListener", + "currentDnsServers", + format!("{:?}", value), + )), + } + } + + fn call_method( + &self, + method: &'static str, + signature: &str, + parameters: &[JValue<'_>], + return_type: JavaType, + ) -> Result<JValue<'_>, Error> { + let env = JnixEnv::from( + self.jvm + .attach_current_thread_as_daemon() + .map_err(Error::AttachJvmToThread)?, + ); + + let method_id = env + .get_method_id(&self.class, method, signature) + .map_err(|cause| Error::FindMethod("ConnectivityListener", method, cause))?; + + env.call_method_unchecked(self.object.as_obj(), method_id, return_type, parameters) + .map_err(|cause| Error::CallMethod("ConnectivityListener", method, cause)) + } +} + +/// Entry point for Android Java code to notify the connectivity status. +#[no_mangle] +#[allow(non_snake_case)] +pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyConnectivityChange( + _: JNIEnv<'_>, + _: JObject<'_>, + connected: jboolean, + sender_address: jlong, +) { + let connected = JNI_TRUE == connected; + let sender_ref = Box::leak(unsafe { get_sender_from_address(sender_address) }); + if let Some(sender) = sender_ref.upgrade() { + if sender + .unbounded_send(Connectivity::Status { connected }) + .is_err() + { + log::warn!("Failed to send offline change event"); + } + } +} + +/// Entry point for Android Java code to return ownership of the sender reference. +#[no_mangle] +#[allow(non_snake_case)] +pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_destroySender( + _: JNIEnv<'_>, + _: JObject<'_>, + sender_address: jlong, +) { + let _ = unsafe { get_sender_from_address(sender_address) }; +} + +unsafe fn get_sender_from_address(address: jlong) -> Box<Weak<UnboundedSender<Connectivity>>> { + Box::from_raw(address as *mut Weak<UnboundedSender<Connectivity>>) +} diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index bb81d7a48f..585c0274ee 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -42,3 +42,7 @@ mod linux; /// A resolver that's controlled by the tunnel state machine #[cfg(target_os = "macos")] pub(crate) mod resolver; + +/// Connectivity monitor for Android +#[cfg(target_os = "android")] +pub mod connectivity_listener; diff --git a/talpid-core/src/offline/android.rs b/talpid-core/src/offline/android.rs index 7dc8389ed3..7280ee792f 100644 --- a/talpid-core/src/offline/android.rs +++ b/talpid-core/src/offline/android.rs @@ -1,217 +1,32 @@ +use crate::connectivity_listener::{ConnectivityListener, Error}; use futures::channel::mpsc::UnboundedSender; -use jnix::{ - jni::{ - self, - objects::{GlobalRef, JObject, JValue}, - signature::{JavaType, Primitive}, - sys::{jboolean, jlong, JNI_TRUE}, - JNIEnv, JavaVM, - }, - JnixEnv, -}; -use std::sync::{Arc, Weak}; -use talpid_types::{android::AndroidContext, net::Connectivity, ErrorExt}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Failed to attach Java VM to tunnel thread")] - AttachJvmToThread(#[source] jni::errors::Error), - - #[error("Failed to call Java method {0}.{1}")] - CallMethod(&'static str, &'static str, #[source] jni::errors::Error), - - #[error("Failed to create global reference to Java object")] - CreateGlobalRef(#[source] jni::errors::Error), - - #[error("Failed to find {0}.{1} method")] - FindMethod(&'static str, &'static str, #[source] jni::errors::Error), - - #[error("Received an invalid result from {0}.{1}: {2}")] - InvalidMethodResult(&'static str, &'static str, String), -} +use talpid_types::net::Connectivity; pub struct MonitorHandle { - jvm: Arc<JavaVM>, - class: GlobalRef, - object: GlobalRef, - _sender: Arc<UnboundedSender<Connectivity>>, + connectivity_listener: ConnectivityListener, } impl MonitorHandle { - pub fn new( - android_context: AndroidContext, - sender: Arc<UnboundedSender<Connectivity>>, - ) -> Result<Self, Error> { - let env = JnixEnv::from( - android_context - .jvm - .attach_current_thread_as_daemon() - .map_err(Error::AttachJvmToThread)?, - ); - - let get_connectivity_listener_method = env - .get_method_id( - &env.get_class("net/mullvad/talpid/TalpidVpnService"), - "getConnectivityListener", - "()Lnet/mullvad/talpid/ConnectivityListener;", - ) - .map_err(|cause| { - Error::FindMethod("MullvadVpnService", "getConnectivityListener", cause) - })?; - - let result = env - .call_method_unchecked( - android_context.vpn_service.as_obj(), - get_connectivity_listener_method, - JavaType::Object("Lnet/mullvad/talpid/ConnectivityListener;".to_owned()), - &[], - ) - .map_err(|cause| { - Error::CallMethod("MullvadVpnService", "getConnectivityListener", cause) - })?; - - let object = match result { - JValue::Object(object) => env.new_global_ref(object).map_err(Error::CreateGlobalRef)?, - value => { - return Err(Error::InvalidMethodResult( - "MullvadVpnService", - "getConnectivityListener", - format!("{:?}", value), - )) - } - }; - - let class = env.get_class("net/mullvad/talpid/ConnectivityListener"); - - Ok(MonitorHandle { - jvm: android_context.jvm, - class, - object, - _sender: sender, - }) + fn new(connectivity_listener: ConnectivityListener) -> Self { + MonitorHandle { + connectivity_listener, + } } #[allow(clippy::unused_async)] pub async fn connectivity(&self) -> Connectivity { - self.get_is_connected() - .map(|connected| Connectivity::Status { connected }) - .unwrap_or_else(|error| { - log::error!( - "{}", - error.display_chain_with_msg("Failed to check connectivity status") - ); - Connectivity::PresumeOnline - }) - } - - fn get_is_connected(&self) -> Result<bool, Error> { - let is_connected = self.call_method( - "isConnected", - "()Z", - &[], - JavaType::Primitive(Primitive::Boolean), - )?; - - match is_connected { - JValue::Bool(JNI_TRUE) => Ok(true), - JValue::Bool(_) => Ok(false), - value => Err(Error::InvalidMethodResult( - "ConnectivityListener", - "isConnected", - format!("{:?}", value), - )), - } - } - - fn set_sender(&self, sender: Weak<UnboundedSender<Connectivity>>) -> Result<(), Error> { - let sender_ptr = Box::new(sender); - let sender_address = Box::into_raw(sender_ptr) as jlong; - - let result = self.call_method( - "setSenderAddress", - "(J)V", - &[JValue::Long(sender_address)], - JavaType::Primitive(Primitive::Void), - )?; - - match result { - JValue::Void => Ok(()), - value => Err(Error::InvalidMethodResult( - "ConnectivityListener", - "setSenderAddress", - format!("{:?}", value), - )), - } - } - - fn call_method( - &self, - method: &'static str, - signature: &str, - parameters: &[JValue<'_>], - return_type: JavaType, - ) -> Result<JValue<'_>, Error> { - let env = JnixEnv::from( - self.jvm - .attach_current_thread_as_daemon() - .map_err(Error::AttachJvmToThread)?, - ); - - let method_id = env - .get_method_id(&self.class, method, signature) - .map_err(|cause| Error::FindMethod("ConnectivityListener", method, cause))?; - - env.call_method_unchecked(self.object.as_obj(), method_id, return_type, parameters) - .map_err(|cause| Error::CallMethod("ConnectivityListener", method, cause)) + self.connectivity_listener.connectivity() } } -/// Entry point for Android Java code to notify the connectivity status. -#[no_mangle] -#[allow(non_snake_case)] -pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_notifyConnectivityChange( - _: JNIEnv<'_>, - _: JObject<'_>, - connected: jboolean, - sender_address: jlong, -) { - let connected = JNI_TRUE == connected; - let sender_ref = Box::leak(unsafe { get_sender_from_address(sender_address) }); - if let Some(sender) = sender_ref.upgrade() { - if sender - .unbounded_send(Connectivity::Status { connected }) - .is_err() - { - log::warn!("Failed to send offline change event"); - } - } -} - -/// Entry point for Android Java code to return ownership of the sender reference. -#[no_mangle] -#[allow(non_snake_case)] -pub extern "system" fn Java_net_mullvad_talpid_ConnectivityListener_destroySender( - _: JNIEnv<'_>, - _: JObject<'_>, - sender_address: jlong, -) { - let _ = unsafe { get_sender_from_address(sender_address) }; -} - -unsafe fn get_sender_from_address(address: jlong) -> Box<Weak<UnboundedSender<Connectivity>>> { - Box::from_raw(address as *mut Weak<UnboundedSender<Connectivity>>) -} - #[allow(clippy::unused_async)] pub async fn spawn_monitor( sender: UnboundedSender<Connectivity>, - android_context: AndroidContext, + connectivity_listener: ConnectivityListener, ) -> Result<MonitorHandle, Error> { - let sender = Arc::new(sender); - let weak_sender = Arc::downgrade(&sender); - let monitor_handle = MonitorHandle::new(android_context, sender)?; - - monitor_handle.set_sender(weak_sender)?; - + let mut monitor_handle = MonitorHandle::new(connectivity_listener); + monitor_handle + .connectivity_listener + .set_connectivity_listener(sender)?; Ok(monitor_handle) } diff --git a/talpid-core/src/offline/mod.rs b/talpid-core/src/offline/mod.rs index 0e1d55c273..6605bd3358 100644 --- a/talpid-core/src/offline/mod.rs +++ b/talpid-core/src/offline/mod.rs @@ -1,9 +1,9 @@ +#[cfg(target_os = "android")] +use crate::connectivity_listener::ConnectivityListener; use futures::channel::mpsc::UnboundedSender; use std::sync::LazyLock; #[cfg(not(target_os = "android"))] use talpid_routing::RouteManagerHandle; -#[cfg(target_os = "android")] -use talpid_types::android::AndroidContext; use talpid_types::{net::Connectivity, ErrorExt}; #[cfg(target_os = "macos")] @@ -44,7 +44,7 @@ pub async fn spawn_monitor( sender: UnboundedSender<Connectivity>, #[cfg(not(target_os = "android"))] route_manager: RouteManagerHandle, #[cfg(target_os = "linux")] fwmark: Option<u32>, - #[cfg(target_os = "android")] android_context: AndroidContext, + #[cfg(target_os = "android")] connectivity_listener: ConnectivityListener, ) -> MonitorHandle { let monitor = if *FORCE_DISABLE_OFFLINE_MONITOR { None @@ -56,7 +56,7 @@ pub async fn spawn_monitor( #[cfg(target_os = "linux")] fwmark, #[cfg(target_os = "android")] - android_context, + connectivity_listener, ) .await .inspect_err(|error| { diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index 6a1d779be8..2541bc88e6 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -49,6 +49,9 @@ use talpid_types::{ tunnel::{ErrorStateCause, ParameterGenerationError, TunnelStateTransition}, }; +#[cfg(target_os = "android")] +use crate::connectivity_listener::ConnectivityListener; + const TUNNEL_STATE_MACHINE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); /// Errors that can happen when setting up or using the state machine. @@ -119,6 +122,7 @@ pub struct LinuxNetworkingIdentifiers { } /// Spawn the tunnel state machine thread, returning a channel for sending tunnel commands. +#[allow(clippy::too_many_arguments)] pub async fn spawn( initial_settings: InitialTunnelState, tunnel_parameters_generator: impl TunnelParametersGenerator, @@ -128,6 +132,7 @@ pub async fn spawn( offline_state_listener: mpsc::UnboundedSender<Connectivity>, #[cfg(target_os = "windows")] volume_update_rx: mpsc::UnboundedReceiver<()>, #[cfg(target_os = "android")] android_context: AndroidContext, + #[cfg(target_os = "android")] connectivity_listener: ConnectivityListener, #[cfg(target_os = "linux")] linux_ids: LinuxNetworkingIdentifiers, ) -> Result<TunnelStateMachineHandle, Error> { let (command_tx, command_rx) = mpsc::unbounded(); @@ -155,7 +160,7 @@ pub async fn spawn( #[cfg(target_os = "windows")] volume_update_rx, #[cfg(target_os = "android")] - android_context, + connectivity_listener, #[cfg(target_os = "linux")] linux_ids, }; @@ -251,7 +256,7 @@ struct TunnelStateMachineInitArgs<G: TunnelParametersGenerator> { #[cfg(target_os = "windows")] volume_update_rx: mpsc::UnboundedReceiver<()>, #[cfg(target_os = "android")] - android_context: AndroidContext, + connectivity_listener: ConnectivityListener, #[cfg(target_os = "linux")] linux_ids: LinuxNetworkingIdentifiers, } @@ -263,7 +268,7 @@ impl TunnelStateMachine { #[cfg(target_os = "windows")] let volume_update_rx = args.volume_update_rx; #[cfg(target_os = "android")] - let android_context = args.android_context; + let connectivity_listener = args.connectivity_listener; let runtime = tokio::runtime::Handle::current(); @@ -339,7 +344,7 @@ impl TunnelStateMachine { #[cfg(target_os = "linux")] Some(args.linux_ids.fwmark), #[cfg(target_os = "android")] - android_context, + connectivity_listener, ) .await; let connectivity = offline_monitor.connectivity().await; |
