diff options
| author | Emīls <emils@mullvad.net> | 2021-12-14 15:05:47 +0000 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2022-01-14 14:42:53 +0000 |
| commit | f46e257b5a0de0b530016672773ecda5d08de9df (patch) | |
| tree | a49c6d32ac3bf56cd1332a34f74388607d8dce8e | |
| parent | c0396210ad8bd9e6e51c4b36975fa7a9de3270ff (diff) | |
| download | mullvadvpn-f46e257b5a0de0b530016672773ecda5d08de9df.tar.xz mullvadvpn-f46e257b5a0de0b530016672773ecda5d08de9df.zip | |
Simplify custom resolver to not leak any traffic
30 files changed, 430 insertions, 1702 deletions
diff --git a/Cargo.lock b/Cargo.lock index a3fdecbf56..aa36088310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2592,7 +2592,6 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "duct", - "either", "err-derive", "futures", "hex", diff --git a/docs/allow-macos-network-check.md b/docs/allow-macos-network-check.md index 82a08afde9..dc2c9cd510 100644 --- a/docs/allow-macos-network-check.md +++ b/docs/allow-macos-network-check.md @@ -5,91 +5,21 @@ requests over the internet before it publishes a default route to the routing ta daemon relies on the routing table to obtain a default route to route traffic to relays and bridges, and since macOS's network reachability seemingly does too, the daemon won't be able to connect to a relay and thus stay in the blocked state for a prolonged time. The default route is only published -when macOS finishes or times out it's captive portal check. The captive portal check involves +when macOS finishes or times out its captive portal check. The captive portal check involves looking up `captive.apple.com` and issuing an HTTP request to the resolved address, and by default, if the app is blocking traffic, none of these network operations can take place, so the timeout is always incurred, which forces the app into the offline error state for a prolonged time. -To not have to wait for macOS to time out it's captive portal check, the app should allow the +To not have to wait for macOS to time out its captive portal check, the app should allow the captive portal check even when it's in a blocking state, whilst still blocking all arbitrary DNS -traffic. This necessitates filtering DNS traffic at the application layer rather than the network -layer, so only the request for `captive.apple.com` is leaked, and before the response is returned, -the firewall rules are updated to allow the resolved addresses from the response to be reachable. +traffic. However, only a DNS response is required to appease the connectivity check - and it doesn't +even need to be valid. As such, during blocked states the app can run a custom resolver that only +responds to queries for `captive.apple.com` to allow macOS to do its connectivity check. Since no +lookups have to be made, no traffic needs to be leaked. -# Leaking macOS network-check traffic - -To allow macOS's network-check to function, _some_ DNS queries need to leaked during a blocked -state. This can be done via using a resolver that is selectively reacts to some DNS queries and is -able to reach upstream resolvers when the app is in a blocking state. For now, this is achieved by -excluding all traffic from a Mullvad specific group, and having the resolver run as part of the -daemon, which asserts the groups ID on startup. The firewall rules that exclude the resolver traffic -and the resolved IP addresses should only be in effect if the app has been configured to allow macOS -network check. When receiving upstream responses, the DNS server in question should first have the -firewall be reconfigured such that the resolved IP addresses are reachable. - -## Filtering resolver's dependencies - -To enable the custom resolver, certain conditions in the rest of the daemon need to be met: -- The firewall must allow traffic coming from our resolver (identified via GID) to the configured - upstream resolvers. The firewall must have a list of IPs for which traffic will be allowed to - pass. The list will be populated by the resolved A and AAAA records, and reset when the tunnel - state machine moves away from the error state. This list will be cleared when moving to any other - tunnel state. -- The daemon must configure the system to use the filtering resolver. -- The resolver must only reply to queries when it's in an active state and it must only reply to - allowed queries. For now, only queries for `captive.apple.com` are allowed. -- The daemon should keep track of *if* the user has enabled the filtering resolver. If the user - enables the custom resolver but something is already listening on port 53, then this should be - reported back to the front-ends. The user needs to know that the filtering resolver failed to run. - - -## Filtering resolver's behavior - -The functionality of this feature is strongly tied to the states of the app when it's blocking -traffic. These blocking states include the app when it's in the disconnected mode with _always -require vpn_ turned on or in an error state with a blocking reason that isn't related to setting DNS -or starting the filtering resolver. In all other tunnel states, the filtering resolver and firewall -rules shouldn't be affected by this feature. - -### When the network-check leak is toggled on - -- When in a blocking state: - 1. Exclude the local resolver's traffic from the firewall. - 1. Configure the filtering resolver to bind to port 53. - 1. Read the system's current DNS config and configure the filtering resolver to use it. - 1. Configure the host to use our local resolver -- In all other states, the filtering resolver should bind to port 53. - -If any of the above steps fail, the app should report the failure to the frontend that toggled the -setting. - -### When the network-check leak is toggled off -- When in a blocking state: - 1. If the host's DNS config is currently using our resolver, this should be reverted. - 1. The firewall should be reset to not allow the resolver traffic and the resolved IP traffic - through. - 1. The filtering resolver should be shut down, unbinding from port 53. -- In all other states, the filtering resolver should be shut down, to leave port 53 free. - -### When the network-check leak is enabled -#### Behavior when the daemon enters a blocking state -To enable the filtering resolver when entering the error state the daemon should do the following: -1. Exclude the local resolver's traffic from the firewall. -1. Read the system's current DNS config and configure the filtering resolver to use it. -1. Configure the host to use our local resolver. - -If any of the above steps fail, and the daemon is not in the disconnected state, it should -transition to an error state and not attempt to start the filtering resolver again. - -#### Resolver's behavior when receiving a DNS query -- When the daemon is in a blocking state, and the query is allowed: - 1. The query should be forwarded to the upstream resolvers - 1. When receiving the response, it's `A` and `AAAA` records should be allowed through the firewall. - 1. The response should be forwarded to the original requester. -- Otherwise, if the network-check allowing is enabled, the response should be ignored. If the - option is disabled, it shouldn't be possible to receive a DNS query. - -#### When the daemon leaves a blocking state: -- The host's DNS configuration is reverted to no longer use the filtering resolver. -- The list of IP addresses that are allowed to pass through our firewall are cleared. +# Overcoming these issues in the daemon. +To allow the connectivity check to pass when blocking traffic, the daemon runs a custom resolver +that listens only on localhost on an arbitrary port. Traffic to it is only redirected during blocked +states. The resolver only replies to queries for `captive.apple.com`. The resolver won't actually +send any packets besides replying to DNS query that originates from localhost. diff --git a/docs/architecture.md b/docs/architecture.md index ecf8ba85fe..c0186f76c9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -120,7 +120,8 @@ they happen on different scenarios and because of different causes. - *Tunnel monitor stopped*: communication to the tunnel monitor was lost - *Is offline*: notify the tunnel state machine if the operating system is connected or not to the network, so that it can safely wait for connectivity to be restored without endlessly retrying to - establish the VPN connection + establish the VPN connection. Some care needs to be taken to not get stuck in the offline state + for too long on [macOS](allow-macos-network-check.md). #### State machine outputs diff --git a/docs/security.md b/docs/security.md index b7ddfea71e..b85b5ef0e6 100644 --- a/docs/security.md +++ b/docs/security.md @@ -240,19 +240,6 @@ The intended use case for this setting is when the user want to only switch betw connectivity at all and using VPN. With this setting active, the device can never communicate with the internet outside of a VPN tunnel. -### macOS network-check - -macOS needs to do a connectivity check before the daemon is able to connect to a tunnel, but the -connectivity check will fail in the blocked state imposing a hefty timeout before a tunnel can be -connected. The connectivity check requires a working DNS resolver and access to `captive.apple.com`. -The feature is discussed in detail [here](allow-macos-network-check.md). - -The app has an option to allow the network check to leak in the error state and during the -disconnected state if _Always require VPN_ is enabled. When the option is enabled, the firewall will -allow all DNS traffic coming from a mullvad specific unix group, and it will allow all traffic to a -set of resolved IP addresses coming from root (as identified by a unix user ID of `0`). - - ## DNS DNS is treated a bit differently from other protocols. Since a user's DNS history can give a diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index e8e4ccb730..de11640587 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -633,10 +633,6 @@ msgctxt "navigation-bar" msgid "Settings" msgstr "" -msgctxt "notifications" -msgid " Unable to activate macOS network check module. Close any programs that might be using port 53, or disable \"Allow macOS network check\"." -msgstr "" - #. The system notification displayed to the user when the account credit is close to expiry. #. Available placeholder: #. %(duration)s - remaining time, e.g. "2 days" @@ -695,10 +691,6 @@ msgid "Disconnected and unsecure" msgstr "" msgctxt "notifications" -msgid "Failed to read system DNS configuration." -msgstr "" - -msgctxt "notifications" msgid "Reconnecting" msgstr "" diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c8cad6fa6a..ac677f4ded 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -831,10 +831,6 @@ function convertFromTunnelStateErrorCause( } case grpcTypes.ErrorState.Cause.SPLIT_TUNNEL_ERROR: return { reason: 'split_tunnel_error' }; - case grpcTypes.ErrorState.Cause.FILTERING_RESOLVER_ERROR: - return { reason: 'filtering_resolver_error' }; - case grpcTypes.ErrorState.Cause.READ_SYSTEM_DNS_CONFIG: - return { reason: 'read_system_dns_config' }; case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: // VPN_PERMISSION_DENIED is only ever created on Android throw invalidErrorStateCause; diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 472742db71..68e61fc3ea 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -41,8 +41,6 @@ export type ErrorStateCause = | 'set_dns_error' | 'start_tunnel_error' | 'is_offline' - | 'filtering_resolver_error' - | 'read_system_dns_config' | 'split_tunnel_error'; } | { reason: 'set_firewall_policy_error'; details: FirewallPolicyError } diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts index 22fd1858dc..066c9c333e 100644 --- a/gui/src/shared/notifications/error.ts +++ b/gui/src/shared/notifications/error.ts @@ -138,16 +138,6 @@ function getMessage(errorDetails: IErrorState, accountExpiry?: string): string { 'notifications', "Your device is offline. Try connecting when it's back online.", ); - case 'filtering_resolver_error': - // TODO: Figure out a better error message to show to users - return messages.pgettext( - 'notifications', - ' Unable to activate macOS network check module. Close any programs that might be using port 53, or disable "Allow macOS network check".', - ); - case 'read_system_dns_config': - // TODO: Figure out a better error message to show to users - return messages.pgettext('notifications', 'Failed to read system DNS configuration.'); - case 'split_tunnel_error': return messages.pgettext( 'notifications', diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 1f0e98f8ed..2ceb3bfdcf 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -28,11 +28,6 @@ pub use self::dns::Dns; mod lan; pub use self::lan::Lan; -#[cfg(target_os = "macos")] -mod network_check; -#[cfg(target_os = "macos")] -pub use self::network_check::NetworkCheck; - mod reconnect; pub use self::reconnect::Reconnect; @@ -69,8 +64,6 @@ pub fn get_commands() -> HashMap<&'static str, Box<dyn Command>> { Box::new(Dns), Box::new(Reconnect), Box::new(Lan), - #[cfg(any(target_os = "macos"))] - Box::new(NetworkCheck), Box::new(Relay), Box::new(Reset), #[cfg(any(target_os = "linux", windows))] diff --git a/mullvad-cli/src/cmds/network_check.rs b/mullvad-cli/src/cmds/network_check.rs deleted file mode 100644 index 323e5eb512..0000000000 --- a/mullvad-cli/src/cmds/network_check.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{new_rpc_client, Command, Result}; -use clap::value_t_or_exit; - -pub struct NetworkCheck; - -const SUBCOMMAND_DESCRIPTION: &'static str = -"Control the macOS network check setting. Allowing the check leaks DNS queries for `captive.apple.com`. Allowing the -connectivity check allows macOS to get online quicker after sleep and after connecting to new WiFi networks"; - -#[mullvad_management_interface::async_trait] -impl Command for NetworkCheck { - fn name(&self) -> &'static str { - "macos-network-check" - } - - fn clap_subcommand(&self) -> clap::App<'static, 'static> { - clap::SubCommand::with_name(self.name()) - .about(SUBCOMMAND_DESCRIPTION) - .setting(clap::AppSettings::SubcommandRequiredElseHelp) - .subcommand( - clap::SubCommand::with_name("set") - .about("Toggle macOS network check setting") - .arg( - clap::Arg::with_name("policy") - .required(true) - .possible_values(&["allow", "block"]), - ), - ) - .subcommand( - clap::SubCommand::with_name("get") - .about("Display current macOS network check setting"), - ) - } - - async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { - if let Some(set_matches) = matches.subcommand_matches("set") { - let allow_network_check = value_t_or_exit!(set_matches.value_of("policy"), String); - self.set(allow_network_check == "allow").await - } else if let Some(_get_matches) = matches.subcommand_matches("get") { - self.get().await - } else { - unreachable!("No macOS network check given") - } - } -} - -impl NetworkCheck { - async fn set(&self, allow_network_check: bool) -> Result<()> { - let mut rpc = new_rpc_client().await?; - rpc.set_allow_macos_network_check(allow_network_check) - .await?; - println!("Changed macOS network check setting"); - Ok(()) - } - - async fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client().await?; - let allow_network_check = rpc - .get_settings(()) - .await? - .into_inner() - .allow_macos_network_check; - println!( - "macOS network check setting: {}", - if allow_network_check { - "allow" - } else { - "block" - } - ); - Ok(()) - } -} diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 57b2c9d890..84b92e910e 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -22,8 +22,6 @@ pub mod settings; pub mod version; mod version_check; -#[cfg(target_os = "macos")] -use either::Either; use futures::{ channel::{mpsc, oneshot}, future::{abortable, AbortHandle, Future}, @@ -251,11 +249,6 @@ pub enum DaemonCommand { /// Set DNS options or servers to use SetDnsOptions(ResponseTx<(), settings::Error>, DnsOptions), /// Toggle macOS network check leak - #[cfg(target_os = "macos")] - SetAllowMacosNetworkCheck( - ResponseTx<(), Either<settings::Error, talpid_core::resolver::Error>>, - bool, - ), /// Set MTU for wireguard tunnels SetWireguardMtu(ResponseTx<(), settings::Error>, Option<u16>), /// Set automatic key rotation interval for wireguard tunnels @@ -688,8 +681,6 @@ where tunnel_state_machine_shutdown_tx, #[cfg(target_os = "macos")] exclusion_gid, - #[cfg(target_os = "macos")] - settings.allow_macos_network_check, #[cfg(target_os = "android")] android_context, ) @@ -1273,11 +1264,6 @@ where SetBridgeState(tx, bridge_state) => self.on_set_bridge_state(tx, bridge_state).await, SetEnableIpv6(tx, enable_ipv6) => self.on_set_enable_ipv6(tx, enable_ipv6).await, SetDnsOptions(tx, dns_servers) => self.on_set_dns_options(tx, dns_servers).await, - #[cfg(target_os = "macos")] - SetAllowMacosNetworkCheck(tx, enable_custom_resolver) => { - self.on_set_allow_macos_network_check(tx, enable_custom_resolver) - .await - } SetWireguardMtu(tx, mtu) => self.on_set_wireguard_mtu(tx, mtu).await, SetWireguardRotationInterval(tx, interval) => { self.on_set_wireguard_rotation_interval(tx, interval).await @@ -2279,50 +2265,6 @@ where } } - #[cfg(target_os = "macos")] - async fn on_set_allow_macos_network_check( - &mut self, - tx: ResponseTx<(), Either<settings::Error, talpid_core::resolver::Error>>, - enable_custom_resolver: bool, - ) { - let result = self - .on_set_custom_resolver_inner(enable_custom_resolver) - .await; - - Self::oneshot_send(tx, result, "on_set_allow_macos_network_check resposne"); - } - - #[cfg(target_os = "macos")] - async fn on_set_custom_resolver_inner( - &mut self, - allow_macos_network_check: bool, - ) -> Result<(), Either<settings::Error, talpid_core::resolver::Error>> { - let (start_tx, start_rx) = oneshot::channel(); - self.send_tunnel_command(TunnelCommand::AllowMacosNetworkCheck( - allow_macos_network_check, - start_tx, - )); - match start_rx.await { - Ok(result) => { - result.map_err(Either::Right)?; - } - Err(_) => { - log::error!("Tunnel state machine has exited"); - return Ok(()); - } - }; - let settings_changed = self - .settings - .set_allow_macos_network_check(allow_macos_network_check) - .await - .map_err(Either::Left)?; - if settings_changed { - self.event_listener - .notify_settings(self.settings.to_settings()); - } - Ok(()) - } - async fn on_set_wireguard_mtu( &mut self, tx: ResponseTx<(), settings::Error>, diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index e27f5fd506..2136312541 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -1,6 +1,4 @@ use crate::{account_history, settings, DaemonCommand, DaemonCommandSender, EventListener}; -#[cfg(target_os = "macos")] -use either::Either; use futures::{ channel::{mpsc, oneshot}, StreamExt, @@ -368,35 +366,6 @@ impl ManagementService for ManagementServiceImpl { Ok(Response::new(())) } - #[cfg(target_os = "macos")] - async fn set_allow_macos_network_check(&self, request: Request<bool>) -> ServiceResult<()> { - let allow_macos_network_check = request.into_inner(); - log::debug!( - "set_allow_macos_network_check({:?})", - allow_macos_network_check - ); - - let (tx, rx) = oneshot::channel(); - self.send_command_to_daemon(DaemonCommand::SetAllowMacosNetworkCheck( - tx, - allow_macos_network_check, - ))?; - self.wait_for_result(rx) - .await? - .map(Response::new) - .map_err(|err| match err { - Either::Right(resolver_error) => { - Status::new(Code::Internal, resolver_error.to_string()) - } - Either::Left(settings_error) => map_settings_error(settings_error), - }) - } - - #[cfg(not(target_os = "macos"))] - async fn set_allow_macos_network_check(&self, _: Request<bool>) -> ServiceResult<()> { - Ok(Response::new(())) - } - // Account management // diff --git a/mullvad-daemon/src/settings.rs b/mullvad-daemon/src/settings.rs index 0465185086..455b1775ed 100644 --- a/mullvad-daemon/src/settings.rs +++ b/mullvad-daemon/src/settings.rs @@ -238,18 +238,6 @@ impl SettingsPersister { self.update(should_save).await } - #[cfg(target_os = "macos")] - pub async fn set_allow_macos_network_check( - &mut self, - allow_macos_network_check: bool, - ) -> Result<bool, Error> { - let should_save = Self::update_field( - &mut self.settings.allow_macos_network_check, - allow_macos_network_check, - ); - self.update(should_save).await - } - pub async fn set_wireguard_mtu(&mut self, mtu: Option<u16>) -> Result<bool, Error> { let should_save = Self::update_field(&mut self.settings.tunnel_options.wireguard.options.mtu, mtu); diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 82a00aaf5e..c4d5575ae3 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -41,7 +41,6 @@ service ManagementService { rpc SetWireguardMtu(google.protobuf.UInt32Value) returns (google.protobuf.Empty) {} rpc SetEnableIpv6(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetDnsOptions(DnsOptions) returns (google.protobuf.Empty) {} - rpc SetAllowMacosNetworkCheck(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} // Account management rpc CreateNewAccount(google.protobuf.Empty) returns (google.protobuf.StringValue) {} @@ -111,8 +110,6 @@ message ErrorState { IS_OFFLINE = 6; VPN_PERMISSION_DENIED = 7; SPLIT_TUNNEL_ERROR = 8; - FILTERING_RESOLVER_ERROR = 9; - READ_SYSTEM_DNS_CONFIG = 10; } enum GenerationError { @@ -275,7 +272,6 @@ message Settings { TunnelOptions tunnel_options = 8; bool show_beta_releases = 9; SplitTunnelSettings split_tunnel = 10; - bool allow_macos_network_check = 11; } message SplitTunnelSettings { diff --git a/mullvad-management-interface/src/types.rs b/mullvad-management-interface/src/types.rs index e8651c11e1..5398927569 100644 --- a/mullvad-management-interface/src/types.rs +++ b/mullvad-management-interface/src/types.rs @@ -149,14 +149,6 @@ impl From<mullvad_types::states::TunnelState> for TunnelState { talpid_tunnel::ErrorStateCause::SplitTunnelError => { i32::from(Cause::SplitTunnelError) } - #[cfg(target_os = "macos")] - talpid_tunnel::ErrorStateCause::FilteringResolverError => { - i32::from(Cause::FilteringResolverError) - } - #[cfg(target_os = "macos")] - talpid_tunnel::ErrorStateCause::ReadSystemDnsConfig => { - i32::from(Cause::ReadSystemDnsConfig) - } }, blocking_error: error_state.block_failure().map(map_firewall_error), auth_fail_reason: if let talpid_tunnel::ErrorStateCause::AuthFailed( @@ -394,11 +386,6 @@ impl From<&mullvad_types::settings::Settings> for Settings { #[cfg(not(windows))] let split_tunnel = None; - #[cfg(not(target_os = "macos"))] - let allow_macos_network_check = false; - #[cfg(target_os = "macos")] - let allow_macos_network_check = settings.allow_macos_network_check; - Self { account_token: settings.get_account_token().unwrap_or_default(), relay_settings: Some(RelaySettings::from(settings.get_relay_settings())), @@ -410,7 +397,6 @@ impl From<&mullvad_types::settings::Settings> for Settings { tunnel_options: Some(TunnelOptions::from(&settings.tunnel_options)), show_beta_releases: settings.show_beta_releases, split_tunnel, - allow_macos_network_check, } } } diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index c5dc7ddda4..f31a53212f 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -78,9 +78,6 @@ pub struct Settings { pub tunnel_options: TunnelOptions, /// Whether to notify users of beta updates. pub show_beta_releases: bool, - #[cfg(target_os = "macos")] - /// Allow leaking some traffic for macOS network check - pub allow_macos_network_check: bool, /// Split tunneling settings #[cfg(windows)] pub split_tunnel: SplitTunnelSettings, @@ -114,8 +111,6 @@ impl Default for Settings { auto_connect: false, tunnel_options: TunnelOptions::default(), show_beta_releases: false, - #[cfg(target_os = "macos")] - allow_macos_network_check: false, #[cfg(windows)] split_tunnel: SplitTunnelSettings::default(), settings_version: CURRENT_SETTINGS_VERSION, diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index ed4cd9b4c1..bcef898a82 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -67,7 +67,6 @@ internet-checksum = "0.2" [target.'cfg(target_os = "macos")'.dependencies] -either = "1" pfctl = "0.4.4" system-configuration = "0.5" trust-dns-server = { git = "https://github.com/mullvad/trust-dns", rev = "c782de0645335d1893a854337b965dd07790c068", features = [ "trust-dns-resolver" ] } diff --git a/talpid-core/src/dns/macos.rs b/talpid-core/src/dns/macos.rs index b3d2a5280f..a33e1d0a4a 100644 --- a/talpid-core/src/dns/macos.rs +++ b/talpid-core/src/dns/macos.rs @@ -1,5 +1,3 @@ -use crate::tunnel_state_machine::TunnelCommand; -use futures::channel::mpsc; use parking_lot::Mutex; use std::{ collections::HashMap, @@ -62,25 +60,6 @@ struct State { dns_settings: DnsSettings, /// The backup of all DNS settings. These are being applied back on reset. backup: HashMap<ServicePath, Option<DnsSettings>>, - /// Tunnel command sender for reporting updates to the system DNS config - tunnel_tx: std::sync::Weak<mpsc::UnboundedSender<crate::tunnel_state_machine::TunnelCommand>>, -} - -impl State { - fn send_new_config(&self) { - if let Some(tunnel_tx) = self.tunnel_tx.upgrade() { - match parse_sc_config(&self.backup) { - Ok(config) => { - // TODO: do better filtering to get the best resolver - let _ = tunnel_tx - .unbounded_send(TunnelCommand::HostDnsConfig(config.into_iter().next())); - } - Err(err) => { - log::error!("Failed to parse host's DNS config: {}", err); - } - }; - } - } } /// Holds the configuration for one service. @@ -223,8 +202,6 @@ pub struct DnsMonitor { /// When it's `Some(state)` we are actively making sure `state.dns_settings` is configured /// on all network interfaces. state: Arc<Mutex<Option<State>>>, - - tunnel_tx: std::sync::Weak<mpsc::UnboundedSender<crate::tunnel_state_machine::TunnelCommand>>, } /// SAFETY: The `SCDynamicStore` can be sent to other threads since it doesn't share mutable state @@ -238,13 +215,12 @@ impl super::DnsMonitorT for DnsMonitor { /// DNS settings for all network interfaces. If any changes occur it will instantly reset /// the DNS settings for that interface back to the last server list set to this instance /// with `set_dns`. - fn new(tunnel_tx: std::sync::Weak<mpsc::UnboundedSender<TunnelCommand>>) -> Result<Self> { + fn new() -> Result<Self> { let state = Arc::new(Mutex::new(None)); Self::spawn(state.clone())?; Ok(DnsMonitor { store: SCDynamicStoreBuilder::new("mullvad-dns").build(), state, - tunnel_tx, }) } @@ -262,7 +238,6 @@ impl super::DnsMonitorT for DnsMonitor { State { dns_settings: settings, backup, - tunnel_tx: self.tunnel_tx.clone(), } } Some(state) => { @@ -273,7 +248,6 @@ impl super::DnsMonitorT for DnsMonitor { State { dns_settings: settings, backup: state.backup, - tunnel_tx: self.tunnel_tx.clone(), } } else { log::debug!("No change, new DNS same as the one already set"); @@ -405,7 +379,6 @@ fn dns_change_callback_internal( changed_keys: CFArray<CFString>, state: &mut State, ) { - state.send_new_config(); for path in &changed_keys { let should_set_dns = match DnsSettings::load(&store, path.clone()).ok() { None => { diff --git a/talpid-core/src/dns/mod.rs b/talpid-core/src/dns/mod.rs index 5a7b5c0dec..a878e01ae8 100644 --- a/talpid-core/src/dns/mod.rs +++ b/talpid-core/src/dns/mod.rs @@ -33,9 +33,6 @@ impl DnsMonitor { pub fn new( #[cfg(target_os = "linux")] handle: tokio::runtime::Handle, #[cfg(target_os = "linux")] route_manager: RouteManagerHandle, - #[cfg(target_os = "macos")] command_tx: std::sync::Weak< - futures::channel::mpsc::UnboundedSender<crate::tunnel_state_machine::TunnelCommand>, - >, ) -> Result<Self, Error> { Ok(DnsMonitor { inner: imp::DnsMonitor::new( @@ -43,8 +40,6 @@ impl DnsMonitor { handle, #[cfg(target_os = "linux")] route_manager, - #[cfg(target_os = "macos")] - command_tx, )?, }) } @@ -87,11 +82,7 @@ trait DnsMonitorT: Sized { ) -> Result<Self, Self::Error>; #[cfg(not(target_os = "linux"))] - fn new( - #[cfg(target_os = "macos")] command_tx: std::sync::Weak< - futures::channel::mpsc::UnboundedSender<crate::tunnel_state_machine::TunnelCommand>, - >, - ) -> Result<Self, Self::Error>; + fn new() -> Result<Self, Self::Error>; fn set(&mut self, interface: &str, servers: &[IpAddr]) -> Result<(), Self::Error>; diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos.rs index b8672a1f92..fc81e47db4 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos.rs @@ -2,7 +2,6 @@ use super::{FirewallArguments, FirewallPolicy, FirewallT}; use ipnetwork::IpNetwork; use pfctl::{DropAction, FilterRuleAction, Uid}; use std::{ - collections::BTreeSet, env, net::{IpAddr, Ipv4Addr}, }; @@ -21,7 +20,8 @@ pub struct Firewall { pf: pfctl::PfCtl, pf_was_enabled: Option<bool>, rule_logging: RuleLogging, - exclusion_gid: u32, + /// An exclusion group ID may be used in the future to help split tunneling in the future. + _exclusion_gid: u32, } impl FirewallT for Firewall { @@ -43,7 +43,7 @@ impl FirewallT for Firewall { pf: pfctl::PfCtl::new()?, pf_was_enabled: None, rule_logging, - exclusion_gid: args.exclusion_gid, + _exclusion_gid: args.exclusion_gid, }) } @@ -70,7 +70,7 @@ impl Firewall { new_filter_rules.append(&mut self.get_allow_loopback_rules()?); new_filter_rules.append(&mut self.get_allow_dhcp_client_rules()?); new_filter_rules.append(&mut self.get_allow_ndp_rules()?); - new_filter_rules.append(&mut self.get_policy_specific_rules(policy)?); + new_filter_rules.append(&mut self.get_policy_specific_rules(&policy)?); let return_out_rule = self .create_rule_builder(FilterRuleAction::Drop(DropAction::Return)) @@ -87,12 +87,34 @@ impl Firewall { let mut anchor_change = pfctl::AnchorChange::new(); anchor_change.set_filter_rules(new_filter_rules); + anchor_change.set_redirect_rules(self.get_dns_redirect_rules(&policy)?); Ok(self.pf.set_rules(ANCHOR_NAME, anchor_change)?) } + fn get_dns_redirect_rules( + &mut self, + policy: &FirewallPolicy, + ) -> Result<Vec<pfctl::RedirectRule>> { + let redirect_rules = match policy { + FirewallPolicy::Blocked { + dns_redirect_port, .. + } => { + vec![pfctl::RedirectRuleBuilder::default() + .action(pfctl::RedirectRuleAction::Redirect) + .interface("lo0") + .proto(pfctl::Proto::Udp) + .to(pfctl::Port::from(53)) + .redirect_to(pfctl::Port::from(*dns_redirect_port)) + .build()?] + } + _ => vec![], + }; + Ok(redirect_rules) + } + fn get_policy_specific_rules( &mut self, - policy: FirewallPolicy, + policy: &FirewallPolicy, ) -> Result<Vec<pfctl::FilterRule>> { match policy { FirewallPolicy::Connecting { @@ -101,7 +123,7 @@ impl Firewall { allow_lan, allowed_endpoint, } => { - let mut rules = vec![self.get_allow_relay_rule(peer_endpoint)?]; + let mut rules = vec![self.get_allow_relay_rule(*peer_endpoint)?]; rules.push(self.get_allowed_endpoint_rule(allowed_endpoint.endpoint)?); // Important to block DNS after allow relay rule (so the relay can operate @@ -112,7 +134,7 @@ impl Firewall { rules.push(self.get_allow_tunnel_rule(&tunnel.interface)?); } - if allow_lan { + if *allow_lan { rules.append(&mut self.get_allow_lan_rules()?); } Ok(rules) @@ -125,11 +147,11 @@ impl Firewall { } => { let mut rules = vec![]; - for server in &dns_servers { + for server in dns_servers.iter() { rules.append(&mut self.get_allow_dns_rules_when_connected(&tunnel, *server)?); } - rules.push(self.get_allow_relay_rule(peer_endpoint)?); + rules.push(self.get_allow_relay_rule(*peer_endpoint)?); // Important to block DNS *before* we allow the tunnel and allow LAN. So DNS // can't leak to the wrong IPs in the tunnel or on the LAN. @@ -137,7 +159,7 @@ impl Firewall { rules.push(self.get_allow_tunnel_rule(tunnel.interface.as_str())?); - if allow_lan { + if *allow_lan { rules.append(&mut self.get_allow_lan_rules()?); } @@ -146,17 +168,12 @@ impl Firewall { FirewallPolicy::Blocked { allow_lan, allowed_endpoint, - allowed_ips, - allow_gid_exclusion_traffic, + .. } => { let mut rules = Vec::new(); rules.push(self.get_allowed_endpoint_rule(allowed_endpoint.endpoint)?); - if allow_gid_exclusion_traffic { - rules.extend(self.get_allow_excluded_dns_rules()?); - rules.extend(self.get_exclusion_rules(&allowed_ips)?); - } - if allow_lan { + if *allow_lan { // Important to block DNS before allow LAN (so DNS does not leak to the LAN) rules.append(&mut self.get_block_dns_rules()?); rules.append(&mut self.get_allow_lan_rules()?); @@ -167,26 +184,6 @@ impl Firewall { } } - /// Constructs rules that allow DNS traffic coming from processes that belong to the excluded - /// group ID to leak. - fn get_allow_excluded_dns_rules(&self) -> Result<[pfctl::FilterRule; 2]> { - let mut builder = self.create_rule_builder(FilterRuleAction::Pass); - - builder.direction(pfctl::Direction::Out); - builder.quick(true); - builder.keep_state(pfctl::StatePolicy::Keep); - builder.to(pfctl::Port::from(53)); - builder.group(self.exclusion_gid); - - Ok([ - builder.proto(pfctl::Proto::Udp).build()?, - builder - .proto(pfctl::Proto::Tcp) - .tcp_flags(Self::get_tcp_flags()) - .build()?, - ]) - } - fn get_allow_dns_rules_when_connected( &self, tunnel: &crate::tunnel::TunnelMetadata, @@ -344,27 +341,6 @@ impl Firewall { Ok(vec![lo0_rule]) } - /// Constructs firewall rules that allow traffic to a set of allowed IP addresses coming from - /// UID 0 processes to leak. - fn get_exclusion_rules( - &self, - allowed_ips: &BTreeSet<IpAddr>, - ) -> Result<Vec<pfctl::FilterRule>> { - let mut vec = Vec::with_capacity(allowed_ips.len()); - for ip in allowed_ips.iter() { - vec.push( - self.create_rule_builder(FilterRuleAction::Pass) - .direction(pfctl::Direction::Out) - .to(*ip) - .quick(true) - .user(Uid::from(super::ROOT_UID)) - .keep_state(pfctl::StatePolicy::Keep) - .build()?, - ); - } - Ok(vec) - } - fn get_allow_lan_rules(&self) -> Result<Vec<pfctl::FilterRule>> { let mut rules = vec![]; for net in &*super::ALLOWED_LAN_NETS { diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 761691e216..7231f8f20c 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -2,8 +2,6 @@ use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; #[cfg(unix)] use lazy_static::lazy_static; -#[cfg(target_os = "macos")] -use std::collections::BTreeSet; use std::fmt; #[cfg(not(target_os = "android"))] use std::net::IpAddr; @@ -140,12 +138,10 @@ pub enum FirewallPolicy { allow_lan: bool, /// Host that should be reachable while in the blocked state. allowed_endpoint: AllowedEndpoint, - /// A list of IPs that can be reached outside the tunnel. - #[cfg(target_os = "macos")] - allowed_ips: BTreeSet<IpAddr>, - /// Enables specific GID exclusion traffic + /// Desination port for DNS traffic redirection. Traffic destined to `127.0.0.1:53` will be + /// redirected to `127.0.0.1:$dns_redirect_port`. #[cfg(target_os = "macos")] - allow_gid_exclusion_traffic: bool, + dns_redirect_port: u16, }, } diff --git a/talpid-core/src/resolver.rs b/talpid-core/src/resolver.rs new file mode 100644 index 0000000000..8338fe2ed6 --- /dev/null +++ b/talpid-core/src/resolver.rs @@ -0,0 +1,340 @@ +use std::{ + io, + net::{Ipv4Addr, SocketAddr}, + str::FromStr, + sync::{Arc, Weak}, +}; + +use std::time::{Duration, Instant}; + +use futures::{ + channel::{mpsc, oneshot}, + SinkExt, StreamExt, +}; + +use trust_dns_server::{ + authority::{ + EmptyLookup, LookupObject, MessageRequest, MessageResponse, MessageResponseBuilder, + }, + client::{ + op::LowerQuery, + rr::{LowerName, RecordType}, + }, + proto::{ + op::{header::MessageType, op_code::OpCode, Header}, + rr::{domain::Name, record_data::RData, Record}, + }, + resolver::lookup::Lookup, + server::{Request, RequestHandler, ResponseHandler, ResponseInfo}, + ServerFuture, +}; + +const ALLOWED_RECORD_TYPES: &[RecordType] = &[RecordType::A, RecordType::AAAA, RecordType::CNAME]; +const CAPTIVE_PORTAL_DOMAIN: &str = "captive.apple.com"; +const TTL_SECONDS: u32 = 3; +/// An IP address to be used in the DNS response to the captive domain query. The address itself +/// belongs to the documentation range so should never be reachable. +const RESOLVED_ADDR: Ipv4Addr = Ipv4Addr::new(198, 51, 100, 1); + +/// Starts a resolver. Returns a cloneable handle, which can activate, deactivate and shut down the +/// resolver. When all instances of a handle are dropped, the server will stop. +pub(crate) async fn start_resolver() -> Result<ResolverHandle, Error> { + let (resolver, resolver_handle) = FilteringResolver::new().await?; + tokio::spawn(resolver.run()); + Ok(resolver_handle) +} + +/// Resolver errors +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +pub enum Error { + /// Failed to bind UDP socket + #[error(display = "Failed to bind UDP socket")] + UdpBindError(#[error(source)] io::Error), + + /// Failed to get local address of a bound UDP socket + #[error(display = "Failed to get local address of a bound UDP socket")] + GetSocketAddrError(#[error(source)] io::Error), +} + +/// A filtering resolver. Listens on a specified port for DNS queries and responds queries for +/// `catpive.apple.com`. Can be toggled to unbind, be bound but not respond or bound and responding +/// to some queries. +struct FilteringResolver { + rx: mpsc::Receiver<ResolverMessage>, + dns_server: Option<(tokio::task::JoinHandle<()>, oneshot::Receiver<()>)>, +} + +/// The `FilteringResolver` is an actor responding to DNS queries. +type ResolverMessage = (LowerQuery, oneshot::Sender<Box<dyn LookupObject>>); + +/// A handle to control a filtering resolver. When all resolver handles are dropped, custom +/// resolver will stop. +#[derive(Clone)] +pub(crate) struct ResolverHandle { + _tx: Arc<mpsc::Sender<ResolverMessage>>, + listening_port: u16, +} + +impl ResolverHandle { + fn new(tx: Arc<mpsc::Sender<ResolverMessage>>, listening_port: u16) -> Self { + Self { + _tx: tx, + listening_port, + } + } + + /// Get listening port for resolver handle + pub fn listening_port(&self) -> u16 { + self.listening_port + } +} + +impl FilteringResolver { + /// Constructs a new filtering resolver and it's handle. + async fn new() -> Result<(Self, ResolverHandle), Error> { + let (tx, rx) = mpsc::channel(0); + let command_tx = Arc::new(tx); + + let mut server = ServerFuture::new(ResolverImpl { + tx: Arc::downgrade(&command_tx), + }); + + let server_listening_socket = + tokio::net::UdpSocket::bind(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0)) + .await + .map_err(Error::UdpBindError)?; + let port = server_listening_socket + .local_addr() + .map_err(Error::GetSocketAddrError)? + .port(); + server.register_socket(server_listening_socket); + + let (server_done_tx, server_done_rx) = oneshot::channel(); + let server_handle = tokio::spawn(async move { + if let Err(err) = server.block_until_done().await { + log::error!("DNS server stopped: {}", err); + } + + let _ = server_done_tx.send(()); + }); + let resolver = Self { + rx, + dns_server: Some((server_handle, server_done_rx)), + }; + + Ok((resolver, ResolverHandle::new(command_tx, port))) + } + + /// Runs the filtering resolver as an actor, listening for new queries instances. When all + /// related [ResolverHandle] instances are dropped, this function will return, closing the DNS + /// server. + async fn run(mut self) { + while let Some((query, tx)) = self.rx.next().await { + self.resolve(query, tx); + } + + if let Some((server_handle, done_rx)) = self.dns_server.take() { + server_handle.abort(); + let _ = done_rx.await; + } + } + + /// Resolvers a query to nothing or a documentation address + fn resolve(&mut self, query: LowerQuery, tx: oneshot::Sender<Box<dyn LookupObject>>) { + if !self.allow_query(&query) { + let _ = tx.send(Box::new(EmptyLookup) as Box<dyn LookupObject>); + return; + } + + let return_query = query.original().clone(); + let mut return_record = Record::with( + return_query.name().clone(), + return_query.query_type(), + TTL_SECONDS, + ); + return_record.set_rdata(RData::A(RESOLVED_ADDR)); + + let lookup = Lookup::new_with_deadline( + return_query, + Arc::new([return_record]), + Instant::now() + Duration::from_secs(3), + ); + let _ = tx.send(Box::new(ForwardLookup(lookup))); + } + + /// Determines whether a DNS query is allowable. Currently, this implies that the query is + /// either a `A`, `AAAA` or a `CNAME` query for `captive.apple.com`. + fn allow_query(&self, query: &LowerQuery) -> bool { + let captive_apple_com: LowerName = + LowerName::from(Name::from_str(CAPTIVE_PORTAL_DOMAIN).unwrap()); + ALLOWED_RECORD_TYPES.contains(&query.query_type()) && query.name() == &captive_apple_com + } +} + +/// An implementation of [trust_dns_server::server::RequestHandler] that forwards queries to +/// `FilteringResolver`. +struct ResolverImpl { + tx: Weak<mpsc::Sender<ResolverMessage>>, +} + +impl ResolverImpl { + fn build_response<'a>( + message: &'a MessageRequest, + lookup: &'a mut Box<dyn LookupObject>, + ) -> MessageResponse<'a, 'a> { + let mut response_header = Header::new(); + response_header.set_id(message.id()); + response_header.set_op_code(OpCode::Query); + response_header.set_message_type(MessageType::Response); + response_header.set_authoritative(false); + + MessageResponseBuilder::from_message_request(message).build( + response_header, + lookup.iter(), + // forwarder responses only contain query answers, no ns,soa or additionals + Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>, + Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>, + Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>, + ) + } + + async fn lookup<R: ResponseHandler>(&self, message: &Request, mut response_handler: R) { + if let Some(tx_ref) = self.tx.upgrade() { + let mut tx = (&*tx_ref).clone(); + let query = message.query(); + let (lookup_tx, lookup_rx) = oneshot::channel(); + let _ = tx.send((query.clone(), lookup_tx)).await; + let mut lookup_result: Box<dyn LookupObject> = lookup_rx + .await + .unwrap_or_else(|_| Box::new(EmptyLookup) as Box<dyn LookupObject>); + let response = Self::build_response(&message, &mut lookup_result); + + if let Err(err) = response_handler.send_response(response).await { + log::error!("Failed to send response: {}", err); + } + } + } +} + +#[async_trait::async_trait] +impl RequestHandler for ResolverImpl { + async fn handle_request<R: ResponseHandler>( + &self, + request: &Request, + response_handle: R, + ) -> ResponseInfo { + if !request.src().ip().is_loopback() { + log::error!("Dropping a stray request from outside: {}", request.src()); + return Header::new().into(); + } + if let MessageType::Query = request.message_type() { + match request.op_code() { + OpCode::Query => { + self.lookup(request, response_handle).await; + } + _ => { + log::trace!("Dropping non-query request: {:?}", request); + } + }; + } + + return Header::new().into(); + } +} + +struct ForwardLookup(Lookup); + +/// This trait has to be reimplemented for the Lookup so that it can be sent back to the +/// RequestHandler implementation. +impl LookupObject for ForwardLookup { + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Record> + Send + 'a> { + Box::new(self.0.record_iter()) + } + + fn take_additionals(&mut self) -> Option<Box<dyn LookupObject>> { + None + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::{mem, net::UdpSocket, thread, time::Duration}; + use trust_dns_server::{ + proto, + resolver::{ + config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, + AsyncResolver, + }, + }; + + async fn start_resolver() -> ResolverHandle { + super::start_resolver().await.unwrap() + } + + async fn get_test_resolver(port: u16) -> trust_dns_server::resolver::TokioAsyncResolver { + let resolver_config = ResolverConfig::from_parts( + None, + vec![], + NameServerConfigGroup::from_ips_clear(&[Ipv4Addr::LOCALHOST.into()], port, true), + ); + AsyncResolver::new( + resolver_config, + ResolverOpts::default(), + proto::TokioRuntime, + ) + .unwrap() + } + + #[test] + fn test_successful_lookup() { + let rt = tokio::runtime::Runtime::new().unwrap(); + let handle = rt.block_on(start_resolver()); + let test_resolver = rt.block_on(get_test_resolver(handle.listening_port())); + + let captive_portal_domain = LowerName::from(Name::from_str(CAPTIVE_PORTAL_DOMAIN).unwrap()); + let resolver_result = rt.block_on(async move { + let dns_request = + test_resolver.lookup(captive_portal_domain, RecordType::A, Default::default()); + + dns_request.await + }); + resolver_result.expect("Failed to resolve test domain"); + } + + #[test] + fn test_failed_lookup() { + let rt = tokio::runtime::Runtime::new().unwrap(); + + let handle = rt.block_on(start_resolver()); + let test_resolver = rt.block_on(get_test_resolver(handle.listening_port())); + + let captive_portal_domain = LowerName::from(Name::from_str("apple.com").unwrap()); + let resolver_result = rt.block_on(async move { + test_resolver + .lookup(captive_portal_domain, RecordType::A, Default::default()) + .await + }); + assert!( + resolver_result.is_err(), + "Non-whitelisted DNS request should fail" + ) + } + + #[test] + fn test_shutdown() { + let rt = tokio::runtime::Runtime::new().unwrap(); + + let handle = rt.block_on(start_resolver()); + let port = handle.listening_port(); + mem::drop(handle); + thread::sleep(Duration::from_millis(300)); + UdpSocket::bind((Ipv4Addr::LOCALHOST, port)) + .expect("Failed to bind to a port that should have been removed"); + } +} diff --git a/talpid-core/src/resolver/mod.rs b/talpid-core/src/resolver/mod.rs deleted file mode 100644 index 8e2731346b..0000000000 --- a/talpid-core/src/resolver/mod.rs +++ /dev/null @@ -1,822 +0,0 @@ -use socket2::{Domain, Socket, Type}; - -use std::{ - collections::BTreeSet, - ffi::CString, - future::Future, - io, - net::{IpAddr, Ipv4Addr, SocketAddr}, - pin::Pin, - str::FromStr, - sync::{Arc, Mutex, Weak}, -}; - -#[cfg(target_os = "macos")] -use std::{ - net, - num::NonZeroU32, - os::unix::io::{FromRawFd, IntoRawFd, RawFd}, -}; - -use futures::{ - channel::{mpsc, oneshot}, - future::Either, - SinkExt, StreamExt, -}; - -use crate::tunnel_state_machine::TunnelCommand; -use trust_dns_server::{ - authority::{ - EmptyLookup, LookupObject, MessageRequest, MessageResponse, MessageResponseBuilder, - }, - client::{ - op::LowerQuery, - rr::{LowerName, RecordType}, - }, - proto::{ - self, - iocompat::AsyncIoTokioAsStd, - op::{header::MessageType, op_code::OpCode, Header}, - rr::{domain::Name, record_data::RData, Record}, - TokioTime, - }, - resolver::{ - config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, - error::ResolveError, - lookup::Lookup, - name_server::{GenericConnection, GenericConnectionProvider}, - AsyncResolver, - }, - server::{Request, RequestHandler, ResponseHandler, ResponseInfo}, - ServerFuture, -}; - -const ALLOWED_RECORD_TYPES: &[RecordType] = &[RecordType::A, RecordType::AAAA, RecordType::CNAME]; -const CAPTIVE_PORTAL_DOMAIN: &str = "captive.apple.com"; - -type TunnelCommandSender = Weak<mpsc::UnboundedSender<TunnelCommand>>; - -/// Starts a resolver. Returns a cloneable handle, which can activate, deactivate and shut down the -/// resolver. When all instances of a handle are dropped, the server will stop. -pub(crate) async fn start_resolver(sender: TunnelCommandSender) -> Result<ResolverHandle, Error> { - start_resolver_inner(sender, 53).await -} - -async fn start_resolver_inner( - tunnel_tx: TunnelCommandSender, - port: u16, -) -> Result<ResolverHandle, Error> { - let (resolver, resolver_handle) = FilteringResolver::new(tunnel_tx, port).await?; - tokio::spawn(resolver.run()); - Ok(resolver_handle) -} - -/// Resolver errors -#[derive(err_derive::Error, Debug)] -#[error(no_from)] -pub enum Error { - /// Failed to launch resolver - #[error(display = "Failed to launch resolver")] - LaunchResolver(#[error(source)] ResolveError), - - /// Failed to bind TCP socket - #[error(display = "Failed to bind TCP socket")] - TcpBindError(#[error(source)] io::Error), - - /// Failed to bind UDP socket - #[error(display = "Failed to bind UDP socket")] - UdpBindError(#[error(source)] io::Error), - - /// Launcher thread panicked - #[error(display = "Panic in the launcher thread")] - LauncherThreadPanic, - - /// The resolver has already shut down - #[error(display = "Resolver is already shut down")] - ResolverShutdown, - - /// System DNS error - #[error(display = "System DNS error")] - SystemDnsError(crate::dns::Error), -} - -impl From<crate::dns::Error> for Error { - fn from(err: crate::dns::Error) -> Self { - Error::SystemDnsError(err) - } -} - -/// A filtering resolver. Listens on a specified port for DNS queries and responds queries for -/// `catpive.apple.com`. Can be toggled to unbind, be bound but not respond or bound and responding -/// to some queries. -struct FilteringResolver { - excluded_resolver: ExcludedUpstreamResolver, - rx: mpsc::Receiver<ResolverMessage>, - resolver_state: ResolverState, - tunnel_tx: TunnelCommandSender, - dns_server: Option<(tokio::task::JoinHandle<()>, oneshot::Receiver<()>)>, - command_sender: Weak<mpsc::Sender<ResolverMessage>>, - runtime_provider: RuntimeProvider, - port: u16, -} - -type OurConnectionProvider = GenericConnectionProvider<RuntimeProvider>; -type ExcludedUpstreamResolver = AsyncResolver<GenericConnection, OurConnectionProvider>; - -/// Resolver state -#[derive(Debug, PartialEq, Clone)] -enum ResolverState { - /// When in an active state, the resolver needs a set of upstream resolvers and the name of the - /// interface it should bind to. - Active(Option<(String, Vec<IpAddr>)>), - /// In the inactive state, the resolver is still listening for DNS queries but it won't be - /// responding to any of them - Inactive, - /// In the shutdown state the resolver is unbound and not listening to queries. - Shutdown, -} - -impl ResolverState { - fn is_running(&self) -> bool { - match self { - Self::Active(_) => true, - _ => false, - } - } -} - -/// The `FilteringResolver` is an actor responding to 2 types of messages: either it's a new DNS -/// query or it's a message to toggle it's state. -enum ResolverMessage { - /// A new DNS query coming in from listener. - Request(LowerQuery, oneshot::Sender<Box<dyn LookupObject>>), - /// Set the resolver's state. - SetResolverState(ResolverState, oneshot::Sender<Result<(), Error>>), -} - -/// A handle to control a filtering resolver -#[derive(Clone)] -pub(crate) struct ResolverHandle { - tx: Arc<mpsc::Sender<ResolverMessage>>, -} - -impl ResolverHandle { - fn new(tx: Arc<mpsc::Sender<ResolverMessage>>) -> Self { - Self { tx } - } - - /// Activate the resolver to have it respond to allowed DNS queries. - pub async fn set_active(&self, config: Option<(String, Vec<IpAddr>)>) -> Result<(), Error> { - self.set_state(ResolverState::Active(config)).await - } - - /// De-activate the resolver to have it ignore all DNS queries. - pub async fn set_inactive(&self) -> Result<(), Error> { - self.set_state(ResolverState::Inactive).await - } - - /// Shut down the resolver so that it no longer listens on port 53. - pub async fn shutdown(&self) -> Result<(), Error> { - self.set_state(ResolverState::Shutdown).await - } - - async fn set_state(&self, state: ResolverState) -> Result<(), Error> { - let (done_tx, done_rx) = oneshot::channel(); - let tx: &mpsc::Sender<ResolverMessage> = &*self.tx; - let mut tx = tx.clone(); - tx.send(ResolverMessage::SetResolverState(state, done_tx)) - .await - .map_err(|_| Error::ResolverShutdown)?; - - done_rx.await.map_err(|_| Error::ResolverShutdown)? - } -} - -impl FilteringResolver { - /// Constructs a new filtering resolver and it's handle. - async fn new( - tunnel_tx: TunnelCommandSender, - port: u16, - ) -> Result<(Self, ResolverHandle), Error> { - let (tx, rx) = mpsc::channel(0); - let command_tx = Arc::new(tx); - - let runtime_provider = RuntimeProvider::new(); - - let resolver_config = ResolverConfig::from_parts( - None, - vec![], - NameServerConfigGroup::from_ips_clear(&[], 53, false), - ); - let resolver = ExcludedUpstreamResolver::new( - resolver_config.clone(), - ResolverOpts::default(), - runtime_provider.clone(), - ) - .map_err(Error::LaunchResolver)?; - - let resolver = Self { - excluded_resolver: resolver, - resolver_state: ResolverState::Shutdown, - rx, - tunnel_tx, - command_sender: Arc::downgrade(&command_tx), - dns_server: None, - runtime_provider, - port, - }; - - Ok((resolver, ResolverHandle::new(command_tx))) - } - - /// Runs the filtering resolver as an actor, listening for new [ResolverMessage] instances. - /// When all related [ResolverHandle] instances are dropped, this function will return. - async fn run(mut self) { - use ResolverMessage::*; - while let Some(message) = self.rx.next().await { - match message { - Request(query, tx) => { - if self.resolver_state.is_running() { - tokio::spawn(self.resolve(query, tx)); - } - } - SetResolverState(resolver_state, tx) => { - match resolver_state { - ResolverState::Shutdown => { - self.stop_server().await; - self.resolver_state = ResolverState::Shutdown; - } - running_state => { - if self.dns_server.is_none() { - if let Err(err) = self.spawn_new_server().await { - let _ = tx.send(Err(err)); - let _ = self.reset_resolver().await; - continue; - } - } - self.resolver_state = running_state; - } - } - match self.reset_resolver().await { - Ok(_) => { - let _ = tx.send(Ok(())); - } - Err(err) => { - let _ = tx.send(Err(err)); - } - } - } - } - } - - std::mem::drop(self); - } - - /// Spawns a new [trust_dns_server::server::ServerFuture], used whenever moving away from the - /// [ResolverState::Shutdown] state. - async fn spawn_new_server(&mut self) -> Result<(), Error> { - self.stop_server().await; - if let Some(tx) = self.command_sender.upgrade() { - let resolver_handle = ResolverImpl { tx }; - let mut server = ServerFuture::new(resolver_handle); - let listening_addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), self.port); - let udp_sock = tokio::net::UdpSocket::bind(listening_addr) - .await - .map_err(Error::UdpBindError)?; - let tcp_sock = tokio::net::TcpListener::bind(listening_addr) - .await - .map_err(Error::TcpBindError)?; - server.register_socket(udp_sock); - server.register_listener(tcp_sock, std::time::Duration::from_secs(1)); - - let (server_done_tx, server_done_rx) = oneshot::channel(); - let server_handle = tokio::spawn(async move { - if let Err(err) = server.block_until_done().await { - log::error!("DNS server stopped: {}", err); - } - let _ = server_done_tx.send(()); - }); - - self.dns_server = Some((server_handle, server_done_rx)); - } - Ok(()) - } - - /// Tries to stop the server future as best as it can. - async fn stop_server(&mut self) { - if let Some((old_server, done_rx)) = self.dns_server.take() { - old_server.abort(); - if done_rx.await.is_err() { - log::error!("Server future was already stopped"); - } - } - } - - /// Resets the current upstream resolver to clear it's config. - async fn reset_resolver(&mut self) -> Result<(), Error> { - log::trace!("Resetting filtering resolver"); - let (best_interface, resolver_addresses) = self.get_resolver_config(); - self.runtime_provider.update_best_interface(best_interface); - let resolver_config = ResolverConfig::from_parts( - None, - vec![], - NameServerConfigGroup::from_ips_clear(resolver_addresses, 53, false), - ); - let mut resolver_options = ResolverOpts::default(); - resolver_options.preserve_intermediates = true; - let resolver = AsyncResolver::new( - resolver_config.clone(), - resolver_options, - self.runtime_provider.clone(), - ) - .map_err(Error::LaunchResolver)?; - self.excluded_resolver = resolver; - Ok(()) - } - - /// Gets the best interface to use and a list of upstream resolver addresses to use when - /// resolving domains. Returns an empty config if the current resolver state isn't - /// [ResolverState::Active]. - fn get_resolver_config(&self) -> (&str, &[IpAddr]) { - match &self.resolver_state { - ResolverState::Active(ref resolvers) => resolvers - .as_ref() - .filter(|(_, addresses)| !addresses.iter().any(|ip| ip.is_loopback())) - .map(|(interface_name, addresses)| (interface_name.as_str(), addresses.as_slice())) - .unwrap_or(("", &[])), - _ => ("", &[]), - } - } - - /// Constructs a lookup future for a given DNS query. - fn resolve( - &mut self, - query: LowerQuery, - tx: oneshot::Sender<Box<dyn LookupObject>>, - ) -> impl Future<Output = ()> { - let empty_response = Box::new(EmptyLookup) as Box<dyn LookupObject>; - if !self.should_service_request(&query) { - let _ = tx.send(empty_response); - return Either::Left(async {}); - } - - log::trace!("Looking up {}", query.name()); - - let unblock_tx = self.tunnel_tx.clone(); - let lookup: Box<dyn Future<Output = Result<Lookup, ResolveError>> + Unpin + Send> = - Box::new(self.excluded_resolver.lookup( - query.name().clone(), - query.query_type(), - Default::default(), - )); - let resolver_state = self.resolver_state.clone(); - Either::Right(async move { - match lookup.await { - Ok(result) => { - let lookup = ForwardLookup(result); - let ip_records = lookup - .iter() - .filter_map(|record| match record.rdata() { - RData::A(ipv4) => Some(IpAddr::from(*ipv4)), - RData::AAAA(ipv6) => Some(IpAddr::from(*ipv6)), - _ => None, - }) - .collect::<BTreeSet<_>>(); - - if !ip_records.is_empty() { - if resolver_state.is_running() { - Self::unblock_ips(unblock_tx, ip_records).await; - } - } - if tx.send(Box::new(lookup)).is_err() { - log::error!("Failed to send response to resolver"); - } - } - Err(err) => { - log::trace!("Failed to resolve {}: {}", query, err); - let _ = tx.send(empty_response); - } - } - }) - } - - /// Unblocks a set of addresses in the firewall by sending a message to the tunnel state - /// machine and waiting for completion. Be careful not to call this from the context of - /// [FilteringResolver::run] and instead call it in a different task, as otherwise a deadlock - /// will occur. - async fn unblock_ips(maybe_tx: TunnelCommandSender, addresses: BTreeSet<IpAddr>) { - let (done_tx, done_rx) = oneshot::channel(); - if maybe_tx - .upgrade() - .and_then(|tx| { - tx.unbounded_send(TunnelCommand::AddAllowedIps(addresses, done_tx)) - .ok() - }) - .is_some() - { - let _ = done_rx.await; - } else { - log::error!("Failed to send IPs to unblocker"); - } - } - - /// Determines whether a query should be responded to given the current state of the resolver - /// and if the query is valid. - fn should_service_request(&self, query: &LowerQuery) -> bool { - self.resolver_state.is_running() && self.allow_query(query) - } - - /// Determines whether a DNS query is allowable. Currently, this implies that the query is - /// either a `A`, `AAAA` or a `CNAME` query for `captive.apple.com`. - fn allow_query(&self, query: &LowerQuery) -> bool { - let captive_apple_com: LowerName = - LowerName::from(Name::from_str(CAPTIVE_PORTAL_DOMAIN).unwrap()); - ALLOWED_RECORD_TYPES.contains(&query.query_type()) && query.name() == &captive_apple_com - } -} - -/// An implementation of [trust_dns_server::server::RequestHandler] that forwards queries to -/// `FilteringResolver` as `ResolverMessage::Request` messages. -struct ResolverImpl { - tx: Arc<mpsc::Sender<ResolverMessage>>, -} - -impl ResolverImpl { - fn build_response<'a>( - message: &'a MessageRequest, - lookup: &'a mut Box<dyn LookupObject>, - ) -> MessageResponse<'a, 'a> { - let mut response_header = Header::new(); - response_header.set_id(message.id()); - response_header.set_op_code(OpCode::Query); - response_header.set_message_type(MessageType::Response); - response_header.set_authoritative(false); - - MessageResponseBuilder::from_message_request(message).build( - response_header, - lookup.iter(), - // forwarder responses only contain query answers, no ns,soa or additionals - Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>, - Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>, - Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>, - ) - } - - async fn lookup<R: ResponseHandler>(&self, message: &Request, mut response_handler: R) { - let tx_ref: &mpsc::Sender<ResolverMessage> = &*self.tx; - let mut tx = tx_ref.clone(); - - let query = message.query(); - let (lookup_tx, lookup_rx) = oneshot::channel(); - let _ = tx - .send(ResolverMessage::Request(query.clone(), lookup_tx)) - .await; - let mut lookup_result: Box<dyn LookupObject> = lookup_rx - .await - .unwrap_or_else(|_| Box::new(EmptyLookup) as Box<dyn LookupObject>); - let response = Self::build_response(&message, &mut lookup_result); - - if let Err(err) = response_handler.send_response(response).await { - log::error!("Failed to send response: {}", err); - } - } -} - -#[async_trait::async_trait] -impl RequestHandler for ResolverImpl { - async fn handle_request<R: ResponseHandler>( - &self, - request: &Request, - response_handle: R, - ) -> ResponseInfo { - if !request.src().ip().is_loopback() { - log::error!("Dropping a stray request from outside: {}", request.src()); - return Header::new().into(); - } - if let MessageType::Query = request.message_type() { - match request.op_code() { - OpCode::Query => { - self.lookup(request, response_handle).await; - } - _ => { - log::trace!("Dropping non-query request: {:?}", request); - } - }; - } - - return Header::new().into(); - } -} - -/// RuntimeProvider is used to construct sockets to reach the upstream resolver. -#[derive(Clone)] -struct RuntimeProvider { - best_interface: Arc<Mutex<Option<NonZeroU32>>>, -} - -impl RuntimeProvider { - fn new() -> Self { - Self { - best_interface: Arc::new(Mutex::new(None)), - } - } - - fn update_best_interface(&self, best_interface: &str) { - let ifname = match CString::new(best_interface) { - Ok(name) => name, - Err(err) => { - log::error!("Failed to construct an interface name CString: {}", err); - return; - } - }; - if let Some(index) = NonZeroU32::new(unsafe { libc::if_nametoindex(ifname.as_ptr()) }) { - *self.best_interface.lock().unwrap() = Some(index); - } - } -} - -impl proto::runtime_provider::RuntimeProvider for RuntimeProvider { - type UdpSocket = tokio::net::UdpSocket; - type TcpConnection = AsyncIoTokioAsStd<tokio::net::TcpStream>; - type Time = TokioTime; - - fn connect_tcp( - &self, - addr: SocketAddr, - ) -> Pin<Box<dyn Future<Output = io::Result<Self::TcpConnection>> + Send>> { - let best_interface = self.best_interface.clone(); - - Box::pin(async move { - let raw_fd = open_socket(addr, Type::STREAM, socket2::Protocol::TCP, best_interface)?; - - let socket = unsafe { tokio::net::TcpSocket::from_raw_fd(raw_fd) }; - socket.connect(addr).await.map(AsyncIoTokioAsStd) - }) - } - - fn bind_udp( - &self, - addr: SocketAddr, - ) -> Pin<Box<dyn Future<Output = io::Result<Self::UdpSocket>> + Send>> { - let best_interface = self.best_interface.clone(); - Box::pin(async move { - let raw_fd = open_socket( - addr, - socket2::Type::DGRAM, - socket2::Protocol::UDP, - best_interface.clone(), - )?; - - let std_socket = unsafe { net::UdpSocket::from_raw_fd(raw_fd) }; - tokio::net::UdpSocket::from_std(std_socket) - }) - } - - fn spawn_bg<F>(&self, f: F) - where - F: Future<Output = Result<(), trust_dns_server::proto::error::ProtoError>> + Send + 'static, - { - tokio::spawn(f); - } -} - -fn open_socket( - addr: SocketAddr, - sock_type: Type, - protocol: socket2::Protocol, - best_interface: Arc<Mutex<Option<NonZeroU32>>>, -) -> io::Result<RawFd> { - let socket = Socket::new(Domain::for_address(addr), sock_type, Some(protocol))?; - - socket.set_nonblocking(true)?; - - match best_interface - .lock() - .expect("best interface lock poisoned") - .as_ref() - { - Some(iface_index) => { - if let Err(err) = socket.bind_device_by_index(Some(*iface_index)) { - log::error!("Failed to bind by index: {}", err); - return Err(err); - } - } - None => { - log::error!("Failed to get best interface index"); - } - }; - Ok(socket.into_raw_fd()) -} - -struct ForwardLookup(Lookup); - -/// This trait has to be reimplemented for the Lookup so that it can be sent back to the -/// RequestHandler implementation. -impl LookupObject for ForwardLookup { - fn is_empty(&self) -> bool { - self.0.is_empty() - } - - fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Record> + Send + 'a> { - Box::new(self.0.record_iter()) - } - - fn take_additionals(&mut self) -> Option<Box<dyn LookupObject>> { - None - } -} - -#[cfg(test)] -mod test { - use super::*; - use std::{fs, net::UdpSocket, process::Command}; - use subslice::SubsliceExt; - - fn random_port() -> u16 { - let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap(); - socket.local_addr().unwrap().port() - } - - const NAMESERVER: &[u8] = b"nameserver"; - - fn read_resolvconf() -> Option<(String, Vec<IpAddr>)> { - let contents = fs::read("/etc/resolv.conf").unwrap(); - let nameserver_index = contents - .find(NAMESERVER) - .expect("Failed to read /etc/resolv.conf"); - let end = contents[nameserver_index..] - .find(b"\n") - .expect("no \n after nameserver") - + nameserver_index; - let ip_addr_subslice = &contents[nameserver_index + NAMESERVER.len()..end]; - - let resolver_ip = - IpAddr::from_str(std::str::from_utf8(ip_addr_subslice).unwrap().trim()).unwrap(); - let route_output = String::from_utf8( - Command::new("route") - .arg("get") - .arg(resolver_ip.to_string()) - .output() - .expect("Failed to run 'route get'") - .stdout, - ) - .unwrap(); - - let mut output_parts = route_output.split_whitespace(); - while let Some(part) = output_parts.next() { - if part.trim() == "interface:" { - return Some((output_parts.next().unwrap().to_string(), vec![resolver_ip])); - } - } - panic!("Couldn't deduce interface") - } - - async fn start_resolver() -> ( - ResolverHandle, - u16, - mpsc::UnboundedReceiver<TunnelCommand>, - Arc<mpsc::UnboundedSender<TunnelCommand>>, - ) { - let (tx, rx) = futures::channel::mpsc::unbounded(); - let tx = Arc::new(tx); - let port = random_port(); - - let resolver_handle = super::start_resolver_inner(Arc::downgrade(&tx), port) - .await - .unwrap(); - (resolver_handle, port, rx, tx) - } - - async fn get_test_resolver(port: u16) -> trust_dns_server::resolver::TokioAsyncResolver { - let resolver_config = ResolverConfig::from_parts( - None, - vec![], - NameServerConfigGroup::from_ips_clear(&[Ipv4Addr::LOCALHOST.into()], port, true), - ); - AsyncResolver::new( - resolver_config, - ResolverOpts::default(), - proto::TokioRuntime, - ) - .unwrap() - } - - #[test] - fn test_successful_lookup() { - let rt = tokio::runtime::Runtime::new().unwrap(); - let (handle, port, mut cmd_rx, _txx) = rt.block_on(start_resolver()); - let test_resolver = rt.block_on(get_test_resolver(port)); - let resolver_config = read_resolvconf(); - rt.block_on(async { handle.set_active(resolver_config).await }) - .expect("failed to make resovler active"); - - let captive_portal_domain = LowerName::from(Name::from_str(CAPTIVE_PORTAL_DOMAIN).unwrap()); - let resolver_result = rt.block_on(async move { - let dns_request = - test_resolver.lookup(captive_portal_domain, RecordType::A, Default::default()); - let unblock_request = cmd_rx.next(); - - use futures::future::Either; - match futures::future::select(dns_request, unblock_request).await { - Either::Left((_resolution_result, _unblock_request_future)) => { - panic!("DNS response recieved before unblocking request") - } - Either::Right((unblock_request, resolution)) => { - std::mem::drop(unblock_request); - resolution.await - } - } - }); - resolver_result.expect("Failed to resolve test domain"); - } - - #[test] - fn test_failed_lookup_when_active() { - let rt = tokio::runtime::Runtime::new().unwrap(); - - let (handle, port, mut cmd_rx, _tx) = rt.block_on(start_resolver()); - let test_resolver = rt.block_on(get_test_resolver(port)); - - let resolver_config = read_resolvconf(); - rt.block_on(async { handle.set_active(resolver_config).await }) - .expect("failed to make resovler active"); - - let captive_portal_domain = LowerName::from(Name::from_str("apple.com").unwrap()); - let resolver_result = rt.block_on(async move { - let dns_request = - test_resolver.lookup(captive_portal_domain, RecordType::A, Default::default()); - let unblock_request = cmd_rx.next(); - - use futures::future::Either; - match futures::future::select(dns_request, unblock_request).await { - Either::Left((dns_response, _unblock_request_future)) => dns_response, - Either::Right((_unblock_request, _resolution)) => { - panic!( - "There should be no unblocking for a request that shouldn't be serviced" - ); - } - } - }); - assert!( - resolver_result.is_err(), - "Non-whitelisted DNS request should fail" - ) - } - - #[test] - fn test_failed_lookup_when_inactive() { - let rt = tokio::runtime::Runtime::new().unwrap(); - - let (handle, port, mut cmd_rx, _tx) = rt.block_on(start_resolver()); - let test_resolver = rt.block_on(get_test_resolver(port)); - - rt.block_on(async { handle.set_inactive().await }) - .expect("failed to make resovler active"); - - let captive_portal_domain = LowerName::from(Name::from_str("apple.com").unwrap()); - let resolver_result = rt.block_on(async move { - let dns_request = - test_resolver.lookup(captive_portal_domain, RecordType::A, Default::default()); - let unblock_request = cmd_rx.next(); - - use futures::future::Either; - match futures::future::select(dns_request, unblock_request).await { - Either::Left((dns_response, _unblock_request_future)) => { - dns_response - } - Either::Right((_unblock_request, _resolution)) => { - panic!("There should be no unblocking for for a request when the resolver is inactive"); - } - } - - }); - assert!( - resolver_result.is_err(), - "Non-whitelisted DNS request should fail" - ) - } - - #[test] - fn test_unbinding() { - let rt = tokio::runtime::Runtime::new().unwrap(); - - let (handle, port, mut _cmd_rx, _tx) = rt.block_on(start_resolver()); - let server_sockaddr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port); - - let _ = UdpSocket::bind(server_sockaddr) - .expect("Failed to bind to resolver socket addr when it should be unbound"); - - rt.block_on(async { handle.set_inactive().await }) - .expect("failed to make resovler active"); - - assert!(UdpSocket::bind(server_sockaddr).is_err()); - - rt.block_on(async { handle.shutdown().await }) - .expect("failed to make resovler active"); - - // macOS takes it sweet time reaping the socket - std::thread::sleep(std::time::Duration::from_millis(300)); - UdpSocket::bind(server_sockaddr) - .expect("Failed to bind to resolver socket addr when it should be unbound"); - } -} diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index e8777b5d14..546f9e92ab 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -185,18 +185,6 @@ impl ConnectedState { use self::EventConsequence::*; match command { - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(_allowed_ips, done_tx)) => { - let _ = done_tx.send(()); - SameState(self.into()) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - let _ = done_tx.send(shared_values.deactivate_filtering_resolver(enable)); - SameState(self.into()) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(_new_config)) => SameState(self.into()), Some(TunnelCommand::AllowLan(allow_lan)) => { if let Err(error_cause) = shared_values.set_allow_lan(allow_lan) { self.disconnect(shared_values, AfterDisconnect::Block(error_cause)) diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 74b6d116b5..2ae1924988 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -268,18 +268,6 @@ impl ConnectingState { use self::EventConsequence::*; match command { - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(_, done_tx)) => { - let _ = done_tx.send(()); - SameState(self.into()) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - let _ = done_tx.send(shared_values.deactivate_filtering_resolver(enable)); - SameState(self.into()) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(_new_config)) => SameState(self.into()), Some(TunnelCommand::AllowLan(allow_lan)) => { if let Err(error_cause) = shared_values.set_allow_lan(allow_lan) { self.disconnect(shared_values, AfterDisconnect::Block(error_cause)) @@ -493,13 +481,6 @@ impl TunnelState for ConnectingState { if shared_values.is_offline { return ErrorState::enter(shared_values, ErrorStateCause::IsOffline); } - #[cfg(target_os = "macos")] - if let Err(err) = shared_values.disable_filtering_resolver() { - log::error!( - "{}", - err.display_chain_with_msg("Failed to disable custom resolver") - ); - } match shared_values .tunnel_parameters_generator .generate(retry_attempt) diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index 58e5aa23ac..3682accc0b 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -2,28 +2,21 @@ use super::{ ConnectingState, ErrorState, EventConsequence, SharedTunnelStateValues, TunnelCommand, TunnelCommandReceiver, TunnelState, TunnelStateTransition, TunnelStateWrapper, }; -use crate::firewall::FirewallPolicy; #[cfg(target_os = "macos")] -use crate::{dns, resolver}; +use crate::dns; +use crate::firewall::FirewallPolicy; use futures::StreamExt; #[cfg(target_os = "macos")] -use std::{ - collections::BTreeSet, - net::{IpAddr, Ipv4Addr}, -}; +use std::net::Ipv4Addr; #[cfg(target_os = "macos")] use talpid_types::tunnel::ErrorStateCause; use talpid_types::ErrorExt; /// No tunnel is running. -pub struct DisconnectedState { - #[cfg(target_os = "macos")] - allowed_ips: BTreeSet<IpAddr>, -} +pub struct DisconnectedState; impl DisconnectedState { fn set_firewall_policy( - &mut self, shared_values: &mut SharedTunnelStateValues, should_reset_firewall: bool, ) { @@ -32,9 +25,7 @@ impl DisconnectedState { allow_lan: shared_values.allow_lan, allowed_endpoint: shared_values.allowed_endpoint.clone(), #[cfg(target_os = "macos")] - allowed_ips: self.allowed_ips.clone(), - #[cfg(target_os = "macos")] - allow_gid_exclusion_traffic: shared_values.enable_filtering_resolver, + dns_redirect_port: shared_values.filtering_resolver.listening_port(), }; let firewall_result = shared_values.firewall.apply_policy(policy).map_err(|e| { @@ -88,27 +79,14 @@ impl DisconnectedState { } } - /// Starts the filtering resolver and configures host to use it. + /// Configures host to use a localhost resolver #[cfg(target_os = "macos")] - fn start_filtering_resolver( - &mut self, + fn setup_local_dns_config( shared_values: &mut SharedTunnelStateValues, - ) -> Result<(), either::Either<resolver::Error, dns::Error>> { - use either::Either; - let system_config = shared_values - .dns_monitor - .get_system_config() - .map_err(Either::Right)?; - - shared_values - .runtime - .block_on(shared_values.filtering_resolver.set_active(system_config)) - .map_err(Either::Left)?; + ) -> Result<(), dns::Error> { shared_values .dns_monitor .set("lo", &[Ipv4Addr::LOCALHOST.into()]) - .map_err(resolver::Error::SystemDnsError) - .map_err(Either::Left) } } @@ -119,21 +97,16 @@ impl TunnelState for DisconnectedState { shared_values: &mut SharedTunnelStateValues, should_reset_firewall: Self::Bootstrap, ) -> (TunnelStateWrapper, TunnelStateTransition) { - let mut disconnected_state = DisconnectedState { - #[cfg(target_os = "macos")] - allowed_ips: BTreeSet::new(), - }; - #[cfg(target_os = "macos")] - if shared_values.enable_filtering_resolver && shared_values.block_when_disconnected { - if let Err(err) = disconnected_state.start_filtering_resolver(shared_values) { + if shared_values.block_when_disconnected { + if let Err(err) = Self::setup_local_dns_config(shared_values) { log::error!( "{}", err.display_chain_with_msg("Failed to start filtering resolver:") ); } } else { - if let Err(error) = shared_values.disable_filtering_resolver() { + if let Err(error) = shared_values.dns_monitor.reset() { log::error!( "{}", error.display_chain_with_msg("Unable to disable filtering resolver") @@ -143,20 +116,20 @@ impl TunnelState for DisconnectedState { #[cfg(windows)] Self::register_split_tunnel_addresses(shared_values, should_reset_firewall); - disconnected_state.set_firewall_policy(shared_values, should_reset_firewall); + Self::set_firewall_policy(shared_values, should_reset_firewall); #[cfg(target_os = "linux")] shared_values.reset_connectivity_check(); #[cfg(target_os = "android")] shared_values.tun_provider.close_tun(); ( - TunnelStateWrapper::from(disconnected_state), + TunnelStateWrapper::from(DisconnectedState), TunnelStateTransition::Disconnected, ) } fn handle_event( - mut self, + self, runtime: &tokio::runtime::Handle, commands: &mut TunnelCommandReceiver, shared_values: &mut SharedTunnelStateValues, @@ -172,13 +145,13 @@ impl TunnelState for DisconnectedState { .set_allow_lan(allow_lan) .expect("Failed to set allow LAN parameter"); - self.set_firewall_policy(shared_values, true); + Self::set_firewall_policy(shared_values, true); } SameState(self.into()) } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.set_allowed_endpoint(endpoint) { - self.set_firewall_policy(shared_values, true); + Self::set_firewall_policy(shared_values, true); } if let Err(_) = tx.send(()) { log::error!("The AllowEndpoint receiver was dropped"); @@ -196,14 +169,20 @@ impl TunnelState for DisconnectedState { Some(TunnelCommand::BlockWhenDisconnected(block_when_disconnected)) => { if shared_values.block_when_disconnected != block_when_disconnected { shared_values.block_when_disconnected = block_when_disconnected; - self.set_firewall_policy(shared_values, true); + Self::set_firewall_policy(shared_values, true); #[cfg(windows)] Self::register_split_tunnel_addresses(shared_values, true); #[cfg(target_os = "macos")] - if block_when_disconnected && shared_values.enable_filtering_resolver { - if let Err(err) = self.start_filtering_resolver(shared_values) { - let block_reason = map_filtering_resolver_start(&err); - return NewState(ErrorState::enter(shared_values, block_reason)); + if block_when_disconnected { + if let Err(err) = Self::setup_local_dns_config(shared_values) { + log::error!( + "{}", + err.display_chain_with_msg("Failed to configure host DNS") + ); + return NewState(ErrorState::enter( + shared_values, + ErrorStateCause::SetDnsError, + )); } } else { Self::reset_dns(shared_values); @@ -230,71 +209,6 @@ impl TunnelState for DisconnectedState { shared_values.split_tunnel.set_paths(&paths, result_tx); SameState(self.into()) } - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - if !enable { - if let Err(err) = shared_values.dns_monitor.reset() { - log::error!( - "{}", - err.display_chain_with_msg("Failed to reset DNS config") - ); - } - if let Err(err) = shared_values.deactivate_filtering_resolver(enable) { - let _ = done_tx.send(Err(err)); - if shared_values.enable_filtering_resolver { - self.set_firewall_policy(shared_values, false); - } - return SameState(self.into()); - }; - } - shared_values.enable_filtering_resolver = enable; - self.set_firewall_policy(shared_values, false); - if shared_values.block_when_disconnected && enable { - if let Err(err) = self.start_filtering_resolver(shared_values) { - log::error!( - "{}", - err.display_chain_with_msg("Failed to start filtering resolver:") - ); - - let error_cause = map_filtering_resolver_start(&err); - let _ = done_tx.send(Err(err.left_or_else(resolver::Error::from))); - return NewState(ErrorState::enter(shared_values, error_cause)); - } - } - let _ = done_tx.send(Ok(())); - SameState(self.into()) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(host_config)) => { - if shared_values.block_when_disconnected && shared_values.enable_filtering_resolver - { - if let Err(err) = shared_values - .runtime - .block_on(shared_values.filtering_resolver.set_active(host_config)) - { - log::error!( - "{}", - err.display_chain_with_msg("Failed to activate filtering resolver") - ); - return NewState(ErrorState::enter( - shared_values, - ErrorStateCause::FilteringResolverError, - )); - } - } - SameState(self.into()) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(allowed_ips, done_tx)) => { - let new_addresses = allowed_ips.iter().any(|ip| self.allowed_ips.insert(*ip)); - if new_addresses { - let _ = self.set_firewall_policy(shared_values, false); - } - let _ = done_tx.send(()); - - SameState(self.into()) - } - None => { Self::reset_dns(shared_values); Finished @@ -303,18 +217,3 @@ impl TunnelState for DisconnectedState { } } } - -/// Maps a DNS or a resovler error to an [ErrorStateCause] to be used when failing to start a -/// filtering resolver. -#[cfg(target_os = "macos")] -fn map_filtering_resolver_start( - err: &either::Either<resolver::Error, dns::Error>, -) -> ErrorStateCause { - match err { - either::Either::Right(_dns_err) => ErrorStateCause::SetDnsError, - either::Either::Left(resolver::Error::SystemDnsError(_)) => { - ErrorStateCause::ReadSystemDnsConfig - } - either::Either::Left(_other_err) => ErrorStateCause::FilteringResolverError, - } -} diff --git a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs index e1fab84ffc..8f6f6ae68b 100644 --- a/talpid-core/src/tunnel_state_machine/disconnecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnecting_state.rs @@ -28,18 +28,6 @@ impl DisconnectingState { self.after_disconnect = match after_disconnect { AfterDisconnect::Nothing => match command { - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(_, done_tx)) => { - let _ = done_tx.send(()); - AfterDisconnect::Nothing - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - let _ = done_tx.send(shared_values.deactivate_filtering_resolver(enable)); - AfterDisconnect::Nothing - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(_new_config)) => AfterDisconnect::Nothing, Some(TunnelCommand::AllowLan(allow_lan)) => { let _ = shared_values.set_allow_lan(allow_lan); AfterDisconnect::Nothing @@ -78,20 +66,6 @@ impl DisconnectingState { } }, AfterDisconnect::Block(reason) => match command { - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(_, done_tx)) => { - let _ = done_tx.send(()); - AfterDisconnect::Block(reason) - } - - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - let _ = done_tx.send(shared_values.deactivate_filtering_resolver(enable)); - AfterDisconnect::Block(reason) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(_new_config)) => AfterDisconnect::Block(reason), - Some(TunnelCommand::AllowLan(allow_lan)) => { let _ = shared_values.set_allow_lan(allow_lan); AfterDisconnect::Block(reason) @@ -139,21 +113,6 @@ impl DisconnectingState { let _ = shared_values.set_allow_lan(allow_lan); AfterDisconnect::Reconnect(retry_attempt) } - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - let _ = done_tx.send(shared_values.deactivate_filtering_resolver(enable)); - AfterDisconnect::Reconnect(retry_attempt) - } - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(_new_config)) => { - AfterDisconnect::Reconnect(retry_attempt) - } - - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(_allowed_ips, done_tx)) => { - let _ = done_tx.send(()); - AfterDisconnect::Reconnect(retry_attempt) - } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { let _ = shared_values.set_allowed_endpoint(endpoint); if let Err(_) = tx.send(()) { diff --git a/talpid-core/src/tunnel_state_machine/error_state.rs b/talpid-core/src/tunnel_state_machine/error_state.rs index 6a32ec157c..a501b21f92 100644 --- a/talpid-core/src/tunnel_state_machine/error_state.rs +++ b/talpid-core/src/tunnel_state_machine/error_state.rs @@ -3,14 +3,9 @@ use super::{ TunnelCommandReceiver, TunnelState, TunnelStateTransition, TunnelStateWrapper, }; use crate::firewall::FirewallPolicy; -#[cfg(target_os = "macos")] -use crate::resolver; use futures::StreamExt; #[cfg(target_os = "macos")] -use std::{ - collections::BTreeSet, - net::{IpAddr, Ipv4Addr}, -}; +use std::net::Ipv4Addr; use talpid_types::{ tunnel::{self as talpid_tunnel, ErrorStateCause, FirewallPolicyError}, ErrorExt, @@ -18,37 +13,18 @@ use talpid_types::{ /// No tunnel is running and all network connections are blocked. pub struct ErrorState { - #[cfg(target_os = "macos")] - allowed_ips: BTreeSet<IpAddr>, block_reason: ErrorStateCause, } impl ErrorState { - fn set_firewall( - &self, - shared_values: &mut SharedTunnelStateValues, - ) -> Result<(), FirewallPolicyError> { - Self::set_firewall_policy( - shared_values, - #[cfg(target_os = "macos")] - self.allowed_ips.clone(), - #[cfg(target_os = "macos")] - shared_values.enable_filtering_resolver, - ) - } - fn set_firewall_policy( shared_values: &mut SharedTunnelStateValues, - #[cfg(target_os = "macos")] allowed_ips: BTreeSet<IpAddr>, - #[cfg(target_os = "macos")] allow_gid_exclusion_traffic: bool, ) -> Result<(), FirewallPolicyError> { let policy = FirewallPolicy::Blocked { allow_lan: shared_values.allow_lan, allowed_endpoint: shared_values.allowed_endpoint.clone(), #[cfg(target_os = "macos")] - allowed_ips, - #[cfg(target_os = "macos")] - allow_gid_exclusion_traffic, + dns_redirect_port: shared_values.filtering_resolver.listening_port(), }; #[cfg(target_os = "linux")] @@ -114,9 +90,7 @@ impl TunnelState for ErrorState { } #[cfg(target_os = "macos")] - let host_config = if shared_values.enable_filtering_resolver - && !block_reason.prevents_filtering_resolver() - { + if !block_reason.prevents_filtering_resolver() { if let Err(err) = shared_values .dns_monitor .set("lo", &[Ipv4Addr::LOCALHOST.into()]) @@ -129,52 +103,10 @@ impl TunnelState for ErrorState { ); return Self::enter(shared_values, ErrorStateCause::SetDnsError); } - match shared_values.dns_monitor.get_system_config() { - Ok(host_config) => host_config, - Err(err) => { - log::error!( - "{}", - err.display_chain_with_msg("Failed to start filtering resolver") - ); - if let Err(err) = shared_values.dns_monitor.reset() { - log::error!( - "{}", - err.display_chain_with_msg( - "Faield to reset DNS after failing to obtain host config" - ) - ); - } - return Self::enter(shared_values, ErrorStateCause::FilteringResolverError); - } - } - } else { - None }; #[cfg(not(target_os = "android"))] - let block_failure = Self::set_firewall_policy( - shared_values, - #[cfg(target_os = "macos")] - BTreeSet::new(), - #[cfg(target_os = "macos")] - shared_values.enable_filtering_resolver, - ) - .err(); - - #[cfg(target_os = "macos")] - if let Some(dns_config) = host_config { - if let Err(err) = shared_values.runtime.block_on( - shared_values - .filtering_resolver - .set_active(Some(dns_config)), - ) { - log::error!( - "{}", - err.display_chain_with_msg("Failed to activate filtering resolver") - ); - return Self::enter(shared_values, ErrorStateCause::FilteringResolverError); - } - } + let block_failure = Self::set_firewall_policy(shared_values).err(); #[cfg(target_os = "android")] let block_failure = if !Self::create_blocking_tun(shared_values) { @@ -185,8 +117,6 @@ impl TunnelState for ErrorState { ( TunnelStateWrapper::from(ErrorState { block_reason: block_reason.clone(), - #[cfg(target_os = "macos")] - allowed_ips: BTreeSet::new(), }), TunnelStateTransition::Error(talpid_tunnel::ErrorState::new( block_reason, @@ -197,7 +127,7 @@ impl TunnelState for ErrorState { #[cfg_attr(not(target_os = "macos"), allow(unused_mut))] fn handle_event( - mut self, + self, runtime: &tokio::runtime::Handle, commands: &mut TunnelCommandReceiver, shared_values: &mut SharedTunnelStateValues, @@ -205,127 +135,17 @@ impl TunnelState for ErrorState { use self::EventConsequence::*; match runtime.block_on(commands.next()) { - #[cfg(target_os = "macos")] - Some(TunnelCommand::AddAllowedIps(allowed_ips, done_tx)) => { - let new_addresses = allowed_ips.iter().any(|ip| self.allowed_ips.insert(*ip)); - if new_addresses { - if let Err(err) = self.set_firewall(shared_values) { - return NewState(Self::enter( - shared_values, - ErrorStateCause::SetFirewallPolicyError(err), - )); - } - } - let _ = done_tx.send(()); - SameState(self.into()) - } - - #[cfg(target_os = "macos")] - Some(TunnelCommand::AllowMacosNetworkCheck(enable, done_tx)) => { - let result = if enable { - shared_values.enable_filtering_resolver = enable; - if let Err(err) = self.set_firewall(shared_values) { - return NewState(ErrorState::enter( - shared_values, - ErrorStateCause::SetFirewallPolicyError(err), - )); - } - - match shared_values.dns_monitor.get_system_config() { - Ok(current_system_config) => { - match shared_values.runtime.block_on( - shared_values - .filtering_resolver - .set_active(current_system_config), - ) { - Ok(_) => { - if let Err(err) = shared_values - .dns_monitor - .set("lo", &[Ipv4Addr::LOCALHOST.into()]) - { - log::error!( - "{}", - err.display_chain_with_msg( - "Failed to configure system to use filtering resolver" - ) - ); - let _ = - done_tx.send(Err(resolver::Error::SystemDnsError(err))); - return NewState(ErrorState::enter( - shared_values, - ErrorStateCause::SetDnsError, - )); - } - Ok(()) - } - - Err(err) => { - log::error!( - "{}", - err.display_chain_with_msg( - "Failed to start filtering resolver" - ) - ); - Err(err) - } - } - } - Err(err) => { - log::error!( - "{}", - err.display_chain_with_msg("Failed to obtain system DNS config") - ); - - let _ = done_tx.send(Err(resolver::Error::SystemDnsError(err))); - return NewState(ErrorState::enter( - shared_values, - ErrorStateCause::ReadSystemDnsConfig, - )); - } - } - } else { - if let Err(err) = shared_values.dns_monitor.reset() { - log::error!( - "{}", - err.display_chain_with_msg("Failed to reset DNS config") - ); - } - shared_values.deactivate_filtering_resolver(enable) - }; - let _ = done_tx.send(result); - SameState(self.into()) - } - - #[cfg(target_os = "macos")] - Some(TunnelCommand::HostDnsConfig(host_config)) => { - if shared_values.enable_filtering_resolver { - if let Err(err) = shared_values - .runtime - .block_on(shared_values.filtering_resolver.set_active(host_config)) - { - log::error!( - "Failed to set apply new DNS config to filtering resolver: {}", - err - ); - return NewState(Self::enter( - shared_values, - ErrorStateCause::FilteringResolverError, - )); - } - } - SameState(self.into()) - } Some(TunnelCommand::AllowLan(allow_lan)) => { if let Err(error_state_cause) = shared_values.set_allow_lan(allow_lan) { NewState(Self::enter(shared_values, error_state_cause)) } else { - let _ = self.set_firewall(shared_values); + let _ = Self::set_firewall_policy(shared_values); SameState(self.into()) } } Some(TunnelCommand::AllowEndpoint(endpoint, tx)) => { if shared_values.set_allowed_endpoint(endpoint) { - let _ = self.set_firewall(shared_values); + let _ = Self::set_firewall_policy(shared_values); #[cfg(target_os = "android")] if !Self::create_blocking_tun(shared_values) { @@ -368,12 +188,6 @@ impl TunnelState for ErrorState { Some(TunnelCommand::Disconnect) | None => { #[cfg(target_os = "linux")] shared_values.reset_connectivity_check(); - #[cfg(target_os = "macos")] - if !shared_values.block_when_disconnected { - if let Err(err) = shared_values.disable_filtering_resolver() { - log::error!("Failed to disable filtering resolver: {}", err); - } - } Self::reset_dns(shared_values); NewState(DisconnectedState::enter(shared_values, true)) } diff --git a/talpid-core/src/tunnel_state_machine/mod.rs b/talpid-core/src/tunnel_state_machine/mod.rs index dd6ee01e52..298f8fc7a6 100644 --- a/talpid-core/src/tunnel_state_machine/mod.rs +++ b/talpid-core/src/tunnel_state_machine/mod.rs @@ -28,8 +28,6 @@ use futures::{ channel::{mpsc, oneshot}, stream, StreamExt, }; -#[cfg(target_os = "macos")] -use std::collections::BTreeSet; #[cfg(target_os = "android")] use std::os::unix::io::RawFd; use std::{collections::HashSet, io, net::IpAddr, path::PathBuf, sync::Arc}; @@ -106,7 +104,6 @@ pub async fn spawn( offline_state_listener: mpsc::UnboundedSender<bool>, shutdown_tx: oneshot::Sender<()>, #[cfg(target_os = "macos")] exclusion_gid: u32, - #[cfg(target_os = "macos")] enable_resolver: bool, #[cfg(target_os = "android")] android_context: AndroidContext, ) -> Result<Arc<mpsc::UnboundedSender<TunnelCommand>>, Error> { let (command_tx, command_rx) = mpsc::unbounded(); @@ -135,8 +132,6 @@ pub async fn spawn( command_rx, #[cfg(target_os = "macos")] exclusion_gid, - #[cfg(target_os = "macos")] - enable_resolver, #[cfg(target_os = "android")] android_context, ) @@ -180,15 +175,6 @@ pub enum TunnelCommand { oneshot::Sender<Result<(), split_tunnel::Error>>, Vec<OsString>, ), - /// Sets IP addresses which should be allowed to pass through the firewall. - #[cfg(target_os = "macos")] - AddAllowedIps(BTreeSet<IpAddr>, oneshot::Sender<()>), - /// Toggles filtering resolver - #[cfg(target_os = "macos")] - AllowMacosNetworkCheck(bool, oneshot::Sender<Result<(), crate::resolver::Error>>), - /// Receive up-to-date system DNS config. It should never contain our changes to the DNS. - #[cfg(target_os = "macos")] - HostDnsConfig(Option<(String, Vec<IpAddr>)>), } type TunnelCommandReceiver = stream::Fuse<mpsc::UnboundedReceiver<TunnelCommand>>; @@ -222,11 +208,13 @@ impl TunnelStateMachine { resource_dir: PathBuf, commands_rx: mpsc::UnboundedReceiver<TunnelCommand>, #[cfg(target_os = "macos")] exclusion_gid: u32, - #[cfg(target_os = "macos")] enable_resolver: bool, #[cfg(target_os = "android")] android_context: AndroidContext, ) -> Result<Self, Error> { let runtime = tokio::runtime::Handle::current(); + #[cfg(target_os = "macos")] + let filtering_resolver = crate::resolver::start_resolver().await?; + #[cfg(windows)] let split_tunnel = split_tunnel::SplitTunnel::new(runtime.clone(), command_tx.clone()) .map_err(Error::InitSplitTunneling)?; @@ -253,14 +241,9 @@ impl TunnelStateMachine { route_manager .handle() .map_err(Error::InitRouteManagerError)?, - #[cfg(target_os = "macos")] - command_tx.clone(), ) .map_err(Error::InitDnsMonitorError)?; - #[cfg(target_os = "macos")] - let filtering_resolver = crate::resolver::start_resolver(command_tx.clone()).await?; - let (offline_tx, mut offline_rx) = mpsc::unbounded(); let initial_offline_state_tx = offline_state_tx.clone(); tokio::spawn(async move { @@ -313,8 +296,6 @@ impl TunnelStateMachine { connectivity_check_was_enabled: None, #[cfg(target_os = "macos")] filtering_resolver, - #[cfg(target_os = "macos")] - enable_filtering_resolver: enable_resolver, }; tokio::task::spawn_blocking(move || { @@ -410,9 +391,6 @@ struct SharedTunnelStateValues { /// Filtering resolver handle #[cfg(target_os = "macos")] filtering_resolver: crate::resolver::ResolverHandle, - /// Whether filtering resolver should be enabled - #[cfg(target_os = "macos")] - enable_filtering_resolver: bool, } impl SharedTunnelStateValues { @@ -438,29 +416,6 @@ impl SharedTunnelStateValues { Ok(()) } - /// Sets the filtering resolver setting and toggles it's state to either inactive or shutdown - /// state. - #[cfg(target_os = "macos")] - pub fn deactivate_filtering_resolver( - &mut self, - enable_resolver: bool, - ) -> Result<(), crate::resolver::Error> { - self.enable_filtering_resolver = enable_resolver; - self.disable_filtering_resolver() - } - - /// Toggles filtering resolver state to either inactive or shutdown. - #[cfg(target_os = "macos")] - pub fn disable_filtering_resolver(&mut self) -> Result<(), crate::resolver::Error> { - if self.enable_filtering_resolver { - self.runtime - .block_on(self.filtering_resolver.set_inactive())?; - } else { - self.runtime.block_on(self.filtering_resolver.shutdown())?; - } - Ok(()) - } - pub fn set_allowed_endpoint(&mut self, endpoint: AllowedEndpoint) -> bool { if self.allowed_endpoint != endpoint { #[cfg(target_os = "android")] diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index 9edf45e07f..6f54c15a26 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -106,19 +106,13 @@ pub enum ErrorStateCause { /// Error reported by split tunnel module. #[cfg(target_os = "windows")] SplitTunnelError, - /// Failed to start filtering resolver - #[cfg(target_os = "macos")] - FilteringResolverError, - /// Failed read system DNS config - #[cfg(target_os = "macos")] - ReadSystemDnsConfig, } impl ErrorStateCause { #[cfg(target_os = "macos")] pub fn prevents_filtering_resolver(&self) -> bool { match self { - Self::FilteringResolverError | Self::ReadSystemDnsConfig | Self::SetDnsError => true, + Self::SetDnsError => true, _ => false, } } @@ -214,10 +208,6 @@ impl fmt::Display for ErrorStateCause { VpnPermissionDenied => "The Android VPN permission was denied when creating the tunnel", #[cfg(target_os = "windows")] SplitTunnelError => "The split tunneling module reported an error", - #[cfg(target_os = "macos")] - FilteringResolverError => "Failed to set up custom resolver", - #[cfg(target_os = "macos")] - ReadSystemDnsConfig => "Failed to read system DNS config", }; write!(f, "{}", description) |
