diff options
| author | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-05-14 21:00:52 +0200 |
|---|---|---|
| committer | Joakim Hulthe <joakim.hulthe@mullvad.net> | 2025-05-14 21:00:52 +0200 |
| commit | e191432724e7c2f07953704a8fc875e4ccf9f55e (patch) | |
| tree | 388d04d931e5706adaa35614a7347299f249718b | |
| parent | d17158a3384e3fd2aa7f2e06ff6aa5bfb728eb66 (diff) | |
| parent | 49f0eea29f68f01dd52b7b783a1dabd88fc964c8 (diff) | |
| download | mullvadvpn-e191432724e7c2f07953704a8fc875e4ccf9f55e.tar.xz mullvadvpn-e191432724e7c2f07953704a8fc875e4ccf9f55e.zip | |
Merge branch 'investigate-dns-issues-tb-3-days-des-2076'
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | Cargo.lock | 128 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | talpid-core/Cargo.toml | 1 | ||||
| -rw-r--r-- | talpid-core/src/dns/macos.rs | 9 | ||||
| -rw-r--r-- | talpid-core/src/firewall/macos.rs | 140 | ||||
| -rw-r--r-- | talpid-core/src/firewall/mod.rs | 26 | ||||
| -rw-r--r-- | talpid-core/src/resolver.rs | 338 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 30 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 6 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/disconnected_state.rs | 8 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/error_state.rs | 27 | ||||
| -rw-r--r-- | talpid-types/src/drop_guard.rs | 39 | ||||
| -rw-r--r-- | talpid-types/src/lib.rs | 2 | ||||
| -rw-r--r-- | talpid-wireguard/src/lib.rs | 5 | ||||
| -rw-r--r-- | test/test-manager/src/tests/helpers.rs | 13 | ||||
| -rw-r--r-- | wireguard-go-rs/Cargo.toml | 2 | ||||
| -rw-r--r-- | wireguard-go-rs/src/lib.rs | 6 | ||||
| -rw-r--r-- | wireguard-go-rs/src/util.rs | 15 |
19 files changed, 523 insertions, 278 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index cba74c621d..518dc9d3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ Line wrap the file at 100 chars. Th uses a lot less CPU to compute the keypair, and the public key sent to the server is drastically smaller. +#### macOS +- Use a local DNS resolver on the 127.0.0.0/8 network, regardless of macOS version. + ### Fixed #### Linux - Fix syntax error in Apparmor profile. @@ -35,6 +38,7 @@ Line wrap the file at 100 chars. Th #### macOS - Fully uninstall the app when it is removed by being dropped in the bin. - Add grace period when best default route goes away to reduce frequency of random reconnects. +- Fix DNS not working briefly after switching networks. ### Security #### macOS diff --git a/Cargo.lock b/Cargo.lock index 708f4b4902..6d761edd51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2390,7 +2390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2489,6 +2489,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] name = "matchit" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3315,6 +3324,16 @@ dependencies = [ ] [[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3521,6 +3540,12 @@ dependencies = [ ] [[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3967,7 +3992,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.8.3", "rusty-fork", "tempfile", "unarray", @@ -4293,8 +4318,17 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -4305,7 +4339,7 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.3", ] [[package]] @@ -4316,6 +4350,12 @@ checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" @@ -4816,6 +4856,15 @@ dependencies = [ ] [[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] name = "shared_child" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5077,6 +5126,7 @@ dependencies = [ "talpid-types", "talpid-windows", "talpid-wireguard", + "test-log", "thiserror 2.0.9", "tokio", "tonic-build", @@ -5347,6 +5397,28 @@ dependencies = [ ] [[package]] +name = "test-log" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" +dependencies = [ + "env_logger 0.11.7", + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] name = "thiserror" version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5387,6 +5459,16 @@ dependencies = [ ] [[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5710,6 +5792,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -5880,6 +5991,12 @@ dependencies = [ ] [[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] name = "vec1" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6647,6 +6764,7 @@ dependencies = [ "anyhow", "log", "maybenot-ffi", + "talpid-types", "thiserror 2.0.9", "windows-sys 0.52.0", "zeroize", @@ -165,6 +165,8 @@ See [this](Release.md) for instructions on how to make a new release. * `netsh`: use the `netsh` program * `tcpip`: set TCP/IP parameters in the registry +- `TALPID_DISABLE_LOCAL_DNS_RESOLVER` - Set this variable to `1` to disable the local DNS resolver (macOS only). + * `TALPID_FORCE_USERSPACE_WIREGUARD` - Forces the daemon to use the userspace implementation of WireGuard on Linux. diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index f22cdc50fa..e2ef68cf86 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -104,4 +104,5 @@ tonic-build = { workspace = true, default-features = false, features = ["transpo [dev-dependencies] +test-log = "0.2.17" tokio = { workspace = true, features = ["io-util", "test-util", "time"] } diff --git a/talpid-core/src/dns/macos.rs b/talpid-core/src/dns/macos.rs index 09d9536836..1d00b3071b 100644 --- a/talpid-core/src/dns/macos.rs +++ b/talpid-core/src/dns/macos.rs @@ -183,15 +183,6 @@ impl State { match settings { // Do nothing if the state is already what we want Some(settings) if settings.server_addresses() == desired_set => (), - // Ignore loopback addresses - Some(settings) - if settings - .server_addresses() - .iter() - .any(|addr| addr.ip().is_loopback()) => - { - log::trace!("Not updating DNS config: localhost is used"); - } // Apply desired state to service _ => { let path_cf = CFString::new(path); diff --git a/talpid-core/src/firewall/macos.rs b/talpid-core/src/firewall/macos.rs index da22008db5..0f22641967 100644 --- a/talpid-core/src/firewall/macos.rs +++ b/talpid-core/src/firewall/macos.rs @@ -1,12 +1,14 @@ use std::env; use std::io; +use std::net::Ipv6Addr; +use std::net::SocketAddr; use std::net::{IpAddr, Ipv4Addr}; use std::ptr; use std::sync::LazyLock; use ipnetwork::IpNetwork; use libc::{c_int, sysctlbyname}; -use pfctl::{DropAction, FilterRuleAction, Ip, RedirectRule, Uid}; +use pfctl::{DropAction, FilterRuleAction, Ip, Uid}; use talpid_types::net::{ AllowedEndpoint, AllowedTunnelTraffic, TransportProtocol, ALLOWED_LAN_MULTICAST_NETS, ALLOWED_LAN_NETS, @@ -47,6 +49,7 @@ pub struct Firewall { pf: pfctl::PfCtl, pf_was_enabled: Option<bool>, rule_logging: RuleLogging, + last_policy: Option<FirewallPolicy>, } impl Firewall { @@ -70,6 +73,7 @@ impl Firewall { pf: pfctl::PfCtl::new()?, pf_was_enabled: None, rule_logging, + last_policy: None, }) } @@ -78,10 +82,16 @@ impl Firewall { self.add_anchor()?; self.set_rules(&policy)?; - if let Err(error) = self.flush_states(&policy) { + let last_policy = self.last_policy.as_ref(); + let last_redirect_interface = last_policy.and_then(|p| p.redirect_interface()); + let is_toggling_split_tunneling = policy.redirect_interface() != last_redirect_interface; + + if let Err(error) = self.flush_states(&policy, is_toggling_split_tunneling) { log::error!("Failed to clear PF connection states: {error}"); } + self.last_policy = Some(policy); + Ok(()) } @@ -90,13 +100,18 @@ impl Firewall { /// PF retains approved connections forever, even after a responsible anchor or rule has been /// removed. Therefore, they should be flushed after every state transition to ensure approved /// states conform to our desired policy. - fn flush_states(&mut self, policy: &FirewallPolicy) -> Result<()> { + fn flush_states( + &mut self, + policy: &FirewallPolicy, + is_toggling_split_tunneling: bool, + ) -> Result<()> { self.pf .get_states()? .into_iter() .filter(|state| { // If we can't parse a state for whatever reason, err on the safe side and keep it - Self::should_delete_state(policy, state).unwrap_or(false) + Self::should_delete_state(policy, state, is_toggling_split_tunneling) + .unwrap_or(false) }) .for_each(|state| { if let Err(error) = self.pf.kill_state(&state) { @@ -110,7 +125,11 @@ impl Firewall { /// Clearing the VPN server connection seems to interrupt ephemeral key exchange on some /// machines, so we kill any state except that one as well as within-tunnel connections that /// should still be allowed. - fn should_delete_state(policy: &FirewallPolicy, state: &pfctl::State) -> Result<bool> { + fn should_delete_state( + policy: &FirewallPolicy, + state: &pfctl::State, + is_toggling_split_tunneling: bool, + ) -> Result<bool> { let allowed_tunnel_traffic = policy.allowed_tunnel_traffic(); let tunnel_ips = policy .tunnel() @@ -126,9 +145,15 @@ impl Firewall { return Ok(false); } - if [5353, 53].contains(&remote_address.port()) { - // Ignore DNS states. The local resolver takes care of everything, - // and PQ seems to timeout if these states are flushed + // Socket addresses for Multicast DNS. + let mdns_port = 5353; + let mdns_addrs = [ + SocketAddr::from((Ipv4Addr::new(224, 0, 0, 251), mdns_port)), + SocketAddr::from((Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 0xfb), mdns_port)), + ]; + + if mdns_addrs.contains(&remote_address) { + // Ignore MDNS states. PQ *seems* to timeout if these states are flushed. return Ok(false); } @@ -158,27 +183,32 @@ impl Firewall { return Ok(true); }; - let should_delete = if tunnel_ips.contains(&local_address.ip()) { - // Tunnel traffic: Clear states except those allowed in the tunnel - // Ephemeral peer exchange becomes unreliable otherwise, when multihop is enabled - match allowed_tunnel_traffic { - AllowedTunnelTraffic::None => true, - AllowedTunnelTraffic::All => false, - AllowedTunnelTraffic::One(endpoint) => endpoint.address != remote_address, - AllowedTunnelTraffic::Two(endpoint1, endpoint2) => { - endpoint1.address != remote_address && endpoint2.address != remote_address + // If split tunneling is being turned on/off, ALL in-tunnel states need to be flushed + // because the state will need to be routed through a different tun device. + let should_delete = + if !is_toggling_split_tunneling && tunnel_ips.contains(&local_address.ip()) { + // Tunnel traffic: Clear states except those allowed in the tunnel. + // Ephemeral peer exchange becomes unreliable otherwise, when multihop is enabled. + match allowed_tunnel_traffic { + AllowedTunnelTraffic::None => true, + AllowedTunnelTraffic::All => false, + AllowedTunnelTraffic::One(endpoint) => endpoint.address != remote_address, + AllowedTunnelTraffic::Two(endpoint1, endpoint2) => { + endpoint1.address != remote_address && endpoint2.address != remote_address + } } - } - } else { - // Non-tunnel traffic: Clear all states except traffic destined for the VPN endpoint - // Ephemeral peer exchange becomes unreliable otherwise - peer.address != remote_address || as_pfctl_proto(peer.protocol) != proto - }; + } else { + // Clear all states except traffic destined for the VPN endpoint. + // Ephemeral peer exchange becomes unreliable otherwise. + peer.address != remote_address || as_pfctl_proto(peer.protocol) != proto + }; Ok(should_delete) } pub fn reset_policy(&mut self) -> Result<()> { + self.last_policy = None; + // Implemented this way to not early return on an error. // We always want all three methods to run, and then return // the first error it encountered, if any. @@ -211,10 +241,15 @@ impl Firewall { let mut anchor_change = pfctl::AnchorChange::new(); anchor_change.set_scrub_rules(Self::get_scrub_rules()?); anchor_change.set_filter_rules(new_filter_rules); - anchor_change.set_redirect_rules(self.get_dns_redirect_rules(policy)?); if *NAT_WORKAROUND { anchor_change.set_nat_rules(self.get_nat_rules(policy)?); + } else { + // Make sure NAT ruleset is empty + anchor_change.set_nat_rules(vec![]); } + // Make sure redirect ruleset is empty + anchor_change.set_redirect_rules(vec![]); + self.pf.set_rules(ANCHOR_NAME, anchor_change)?; Ok(()) @@ -229,53 +264,6 @@ impl Firewall { Ok(vec![scrub_rule]) } - fn get_dns_redirect_rules( - &mut self, - policy: &FirewallPolicy, - ) -> Result<Vec<pfctl::RedirectRule>> { - /// Redirect DNS requests to `port`. Technically this redirects UDP on port 53 to `port`. - /// - /// For this to work as expected, please make sure a DNS resolver is running on `port`. - fn redirect_dns_to(port: u16) -> Result<Vec<RedirectRule>> { - let redirect_dns = pfctl::RedirectRuleBuilder::default() - .action(pfctl::RedirectRuleAction::Redirect) - .interface("lo0") - .proto(pfctl::Proto::Udp) - .to(pfctl::Port::from(53)) - .redirect_to(pfctl::Port::from(port)) - .build()?; - Ok(vec![redirect_dns]) - } - - let redirect_rules = if *crate::resolver::LOCAL_DNS_RESOLVER { - match policy { - FirewallPolicy::Connected { dns_config, .. } if dns_config.is_loopback() => { - vec![] - } - FirewallPolicy::Blocked { - dns_redirect_port, .. - } - | FirewallPolicy::Connecting { - dns_redirect_port, .. - } - | FirewallPolicy::Connected { - dns_redirect_port, .. - } => redirect_dns_to(*dns_redirect_port)?, - } - } else { - // Only apply redirect rules in the blocked state if we should *not* use our local DNS - // resolver, since it will be running in the blocked state to work with Apple's captive - // portal check. - match policy { - FirewallPolicy::Blocked { - dns_redirect_port, .. - } => redirect_dns_to(*dns_redirect_port)?, - FirewallPolicy::Connecting { .. } | FirewallPolicy::Connected { .. } => vec![], - } - }; - Ok(redirect_rules) - } - /// Force all traffic out on the VPN interface (except LAN and some other exceptions). /// /// Some programs have been shown to bind their sockets directly to the physical network @@ -370,7 +358,6 @@ impl Firewall { allowed_endpoint, allowed_tunnel_traffic, redirect_interface, - dns_redirect_port: _, } => { let mut rules = vec![self.get_allow_relay_rule(peer_endpoint)?]; rules.push(self.get_allowed_endpoint_rule(allowed_endpoint)?); @@ -415,7 +402,6 @@ impl Firewall { allow_lan, dns_config, redirect_interface, - dns_redirect_port: _, } => { let mut rules = vec![]; @@ -926,9 +912,9 @@ impl Firewall { // remove_anchor() does not deactivate active rules self.pf .flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Filter)?; - if *NAT_WORKAROUND { - self.pf.flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Nat)?; - } + self.pf.flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Nat)?; + self.pf + .flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Redirect)?; self.pf .flush_rules(ANCHOR_NAME, pfctl::RulesetKind::Scrub)?; Ok(()) @@ -967,8 +953,6 @@ impl Firewall { } self.pf .try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Filter)?; - self.pf - .try_add_anchor(ANCHOR_NAME, pfctl::AnchorKind::Redirect)?; Ok(()) } diff --git a/talpid-core/src/firewall/mod.rs b/talpid-core/src/firewall/mod.rs index 0bcbf3191e..053317f2a5 100644 --- a/talpid-core/src/firewall/mod.rs +++ b/talpid-core/src/firewall/mod.rs @@ -94,10 +94,6 @@ pub enum FirewallPolicy { /// Interface to redirect (VPN tunnel) traffic to #[cfg(target_os = "macos")] redirect_interface: Option<String>, - /// Destination 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")] - dns_redirect_port: u16, }, /// Allow traffic only to server and over tunnel interface @@ -114,10 +110,6 @@ pub enum FirewallPolicy { /// Interface to redirect (VPN tunnel) traffic to #[cfg(target_os = "macos")] redirect_interface: Option<String>, - /// Destination 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")] - dns_redirect_port: u16, }, /// Block all network traffic in and out from the computer. @@ -126,10 +118,6 @@ pub enum FirewallPolicy { allow_lan: bool, /// Host that should be reachable while in the blocked state. allowed_endpoint: Option<AllowedEndpoint>, - /// Destination 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")] - dns_redirect_port: u16, }, } @@ -189,6 +177,20 @@ impl FirewallPolicy { | FirewallPolicy::Blocked { allow_lan, .. } => *allow_lan, } } + + /// Return the interface to redirect (VPN tunnel) traffic to, if any. + #[cfg(target_os = "macos")] + pub fn redirect_interface(&self) -> Option<&str> { + match self { + FirewallPolicy::Connecting { + redirect_interface, .. + } => redirect_interface.as_deref(), + FirewallPolicy::Connected { + redirect_interface, .. + } => redirect_interface.as_deref(), + FirewallPolicy::Blocked { .. } => None, + } + } } impl fmt::Display for FirewallPolicy { diff --git a/talpid-core/src/resolver.rs b/talpid-core/src/resolver.rs index 77b416fb95..0f51e5023e 100644 --- a/talpid-core/src/resolver.rs +++ b/talpid-core/src/resolver.rs @@ -16,7 +16,6 @@ use std::{ use futures::{ channel::{mpsc, oneshot}, - future::Either, SinkExt, StreamExt, }; @@ -41,7 +40,14 @@ use hickory_server::{ server::{Request, RequestHandler, ResponseHandler, ResponseInfo}, ServerFuture, }; +use rand::random; use std::sync::LazyLock; +use talpid_types::drop_guard::{on_drop, OnDrop}; +use tokio::{ + net::{self, UdpSocket}, + process::Command, + task::JoinHandle, +}; /// If a local DNS resolver should be used at all times. /// @@ -49,34 +55,27 @@ use std::sync::LazyLock; /// the local DNS resoler to work around Apple's captive portals check. Exactly how this is done is /// documented elsewhere. pub static LOCAL_DNS_RESOLVER: LazyLock<bool> = LazyLock::new(|| { - use talpid_platform_metadata::MacosVersion; - let version = MacosVersion::new().expect("Could not detect macOS version"); - let v = |s| MacosVersion::from_raw_version(s).unwrap(); - // Apple services tried to perform DNS lookups on the physical interface on some macOS - // versions, so we added redirect rules to always redirect DNS to our local DNS resolver. - // This seems to break some apps which do not like that we redirect DNS on port 53 to our local - // DNS resolver running on some other, arbitrary port, and so we disable this behaviour on - // macOS versions that are unaffected by this naughty bug. - // - // The workaround should only be applied to the affected macOS versions because some programs - // set the `skip filtering` pf flag on loopback, which meant that the pf filtering would break - // unexpectedly. We could clear the `skip filtering` flag to force pf filtering on loopback, - // but apparently it is good practice to enable `skip filtering` on loopback so we decided - // against this. Source: https://www.openbsd.org/faq/pf/filter.html - // - // It should be noted that most programs still works fine with this workaround enabled. Notably - // programs that use `getaddrinfo` would behave correctly when we redirect DNS to our local - // resolver, while some programs always used port 53 no matter what (nslookup for example). - // Also, most programs don't set the `skip filtering` pf flag on loopback, but some notable - // ones do for some reason. Orbstack is one such example, which meant that people running - // containers would run into the aforementioned issue. - let use_local_dns_resolver = v("14.6") <= version && version < v("15.1"); - if use_local_dns_resolver { + let disable_local_dns_resolver = std::env::var("TALPID_DISABLE_LOCAL_DNS_RESOLVER") + .map(|v| v != "0") + // Use the local DNS resolver by default. + .unwrap_or(false); + + if !disable_local_dns_resolver { log::debug!("Using local DNS resolver"); } - use_local_dns_resolver + !disable_local_dns_resolver }); +// Name of the loopback network device. +const LOOPBACK: &str = "lo0"; + +/// The port we should bind the local DNS resolver to. +const DNS_PORT: u16 = if cfg!(test) { + 1053 // use a value above 1000 to allow for running the tests without root privileges +} else { + 53 +}; + const ALLOWED_RECORD_TYPES: &[RecordType] = &[RecordType::A, RecordType::CNAME]; const CAPTIVE_PORTAL_DOMAINS: &[&str] = &["captive.apple.com", "netcts.cdn-apple.com"]; @@ -105,20 +104,23 @@ pub async fn start_resolver() -> Result<ResolverHandle, Error> { pub enum Error { /// Failed to bind UDP socket #[error("Failed to bind UDP socket")] - UdpBindError(#[source] io::Error), + UdpBind, /// Failed to get local address of a bound UDP socket #[error("Failed to get local address of a bound UDP socket")] - GetSocketAddrError(#[source] io::Error), + GetSocketAddr(#[source] io::Error), } /// A DNS resolver that forwards queries to some other DNS server /// /// Is controlled by commands sent through [ResolverHandle]s. +/// When all [ResolverHandle]s are dropped, [Self::rx] will close and [Self::run] will exit. struct LocalResolver { rx: mpsc::UnboundedReceiver<ResolverMessage>, - dns_server: Option<(tokio::task::JoinHandle<()>, oneshot::Receiver<()>)>, + dns_server_task: JoinHandle<()>, inner_resolver: Resolver, + /// Which IP+port the local resolver is bound to. + bound_to: SocketAddr, } /// A message to [LocalResolver] @@ -162,45 +164,24 @@ enum Resolver { Forwarding(TokioAsyncResolver), } -impl From<Config> for Resolver { - fn from(mut config: Config) -> Self { - match &mut config { - Config::Blocking => Resolver::Blocking, - Config::Forwarding { dns_servers } => { - // make sure not to accidentally forward queries to ourselves - dns_servers.retain(|addr| !addr.is_loopback()); - - let forward_server_config = - NameServerConfigGroup::from_ips_clear(dns_servers, 53, true); - - let forward_config = - ResolverConfig::from_parts(None, vec![], forward_server_config); - let resolver_opts = ResolverOpts::default(); - - let resolver = TokioAsyncResolver::tokio(forward_config, resolver_opts); - - Resolver::Forwarding(resolver) - } - } - } -} - impl Resolver { pub fn resolve( &self, query: LowerQuery, tx: oneshot::Sender<std::result::Result<Box<dyn LookupObject>, ResolveError>>, ) { - let lookup = match self { - Resolver::Blocking => Either::Left(async move { Self::resolve_blocked(query) }), + match self { + Resolver::Blocking => { + let _ = tx.send(Self::resolve_blocked(query)); + } Resolver::Forwarding(resolver) => { - Either::Right(Self::resolve_forward(resolver.clone(), query)) + let resolver = resolver.clone(); + tokio::spawn(async move { + let lookup = Self::resolve_forward(resolver, query); + let _ = tx.send(lookup.await); + }); } }; - - tokio::spawn(async move { - let _ = tx.send(lookup.await); - }); } /// Resolution in blocked state will return spoofed records for captive portal domains. @@ -259,17 +240,17 @@ impl Resolver { #[derive(Clone)] pub struct ResolverHandle { tx: Arc<mpsc::UnboundedSender<ResolverMessage>>, - listening_port: u16, + listening_addr: SocketAddr, } impl ResolverHandle { - fn new(tx: Arc<mpsc::UnboundedSender<ResolverMessage>>, listening_port: u16) -> Self { - Self { tx, listening_port } + fn new(tx: Arc<mpsc::UnboundedSender<ResolverMessage>>, listening_addr: SocketAddr) -> Self { + Self { tx, listening_addr } } - /// Get listening port for resolver handle - pub fn listening_port(&self) -> u16 { - self.listening_port + /// Get socket address associated with the running DNS resolver. + pub fn listening_addr(&self) -> SocketAddr { + self.listening_addr } /// Set the DNS server to forward queries to `dns_servers` @@ -298,69 +279,160 @@ impl ResolverHandle { impl LocalResolver { /// Constructs a new filtering resolver and it's handle. async fn new() -> Result<(Self, ResolverHandle), Error> { - let (tx, rx) = mpsc::unbounded(); - let command_tx = Arc::new(tx); - + let (command_tx, command_rx) = mpsc::unbounded(); + let command_tx = Arc::new(command_tx); let weak_tx = Arc::downgrade(&command_tx); - let (mut server, port) = Self::new_server(0, weak_tx.clone()).await?; - let (server_done_tx, server_done_rx) = oneshot::channel(); - let server_handle = tokio::spawn(async move { + let (socket, cleanup_ifconfig) = Self::new_random_socket().await?; + let resolver_addr = socket.local_addr().map_err(Error::GetSocketAddr)?; + let mut server = Self::new_server(socket, weak_tx.clone())?; + + let dns_server_task = tokio::spawn(async move { + // This drop guard will clean up the loopback IP addr alias when the task exits. + let _cleanup_ifconfig = cleanup_ifconfig; + + log::info!("Running DNS resolver on {resolver_addr}"); + loop { - if let Err(err) = server.block_until_done().await { - log::error!("DNS server unexpectedly stopped: {}", err); + let Err(err) = server.block_until_done().await else { + break; // Graceful shutdown + }; + + log::error!("DNS server unexpectedly stopped: {}", err); + drop(server); // drop the old server since we need to create a new one + + // Exit if `command_tx` has been dropped. + if weak_tx.strong_count() == 0 { + break; + } + + log::debug!("Attempting to restart server"); + + let socket = match net::UdpSocket::bind(resolver_addr).await { + Ok(socket) => socket, + Err(e) => { + log::error!("Failed to bind DNS server to {resolver_addr}: {e}"); + break; + } + }; - if weak_tx.strong_count() > 0 { - log::debug!("Attempting restart server"); - match Self::new_server(port, weak_tx.clone()).await { - Ok((new_server, _port)) => { - server = new_server; - continue; - } - Err(error) => { - log::error!("Failed to restart DNS server: {error}"); - } - } + match Self::new_server(socket, weak_tx.clone()) { + Ok(new_server) => server = new_server, + Err(error) => { + log::error!("Failed to restart DNS server: {error}"); + break; } } - break; } - - let _ = server_done_tx.send(()); }); let resolver = Self { - rx, - dns_server: Some((server_handle, server_done_rx)), - inner_resolver: Resolver::from(Config::Blocking), + rx: command_rx, + dns_server_task, + bound_to: resolver_addr, + inner_resolver: Resolver::Blocking, }; - Ok((resolver, ResolverHandle::new(command_tx, port))) + Ok((resolver, ResolverHandle::new(command_tx, resolver_addr))) } - async fn new_server( - port: u16, + fn new_server( + socket: UdpSocket, command_tx: Weak<mpsc::UnboundedSender<ResolverMessage>>, - ) -> Result<(ServerFuture<ResolverImpl>, u16), Error> { + ) -> Result<ServerFuture<ResolverImpl>, Error> { let mut server = ServerFuture::new(ResolverImpl { tx: command_tx }); - let server_listening_socket = - tokio::net::UdpSocket::bind(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port)) + server.register_socket(socket); + + Ok(server) + } + + /// Create a new [net::UdpSocket] bound to port 53 on loopback. + /// + /// This socket will try to bind to the following IPs in sequential order: + /// - random ip in the range 127.1-255.0-255.0-255 : 53 + /// - random ip in the range 127.1-255.0-255.0-255 : 53 + /// - random ip in the range 127.1-255.0-255.0-255 : 53 + /// - 127.0.0.1 : 53 + /// + /// We do this to try and avoid collisions with other DNS servers running on the same system. + /// + /// # Returns + /// - The first successfully bound [UdpSocket] + /// - An [OnDrop] guard that will delete the IP aliases added, if any. + /// If the guard is dropped while the socket is in use, calls to read/write will likely fail. + async fn new_random_socket() -> Result<(UdpSocket, OnDrop), Error> { + use std::net::Ipv4Addr; + + let random_loopback = || async move { + let addr = Ipv4Addr::new(127, 1u8.max(random()), random(), random()); + + // TODO: this command requires root privileges and will thus not work in `cargo test`. + // This means that the tests will fall back to 127.0.0.1, and will not assert that the + // ifconfig stuff actually works. We probably do want to test this, so what do? + let output = Command::new("ifconfig") + .args([LOOPBACK, "alias", &format!("{addr}"), "up"]) + .output() .await - .map_err(Error::UdpBindError)?; - let port = server_listening_socket - .local_addr() - .map_err(Error::GetSocketAddrError)? - .port(); - server.register_socket(server_listening_socket); + .inspect_err(|e| { + log::warn!("Failed to spawn `ifconfig {LOOPBACK} alias {addr} up`: {e}") + }) + .ok()?; + + if !output.status.success() { + log::warn!("Non-zero exit code from ifconfig: {}", output.status); + return None; + } + + log::debug!("Created loopback address {addr}"); + + // Clean up ip address when stopping the resolver + let cleanup_ifconfig = on_drop(move || { + tokio::task::spawn(async move { + log::debug!("Cleaning up loopback address {addr}"); + + let result = Command::new("ifconfig") + .args([LOOPBACK, "delete", &format!("{addr}")]) + .output() + .await; - Ok((server, port)) + if let Err(e) = result { + log::warn!("Failed to clean up {LOOPBACK} alias {addr}: {e}"); + } + }); + }) + .boxed(); + + Some((addr, cleanup_ifconfig)) + }; + + for attempt in 0.. { + let (socket_addr, on_drop) = match attempt { + ..3 => match random_loopback().await { + Some(random) => random, + None => continue, + }, + 3 => (Ipv4Addr::LOCALHOST, OnDrop::noop()), + 4.. => break, + }; + + match net::UdpSocket::bind((socket_addr, DNS_PORT)).await { + Ok(socket) => return Ok((socket, on_drop)), + Err(err) => log::warn!("Failed to bind DNS server to {socket_addr}: {err}"), + } + } + + // See logs for details. + Err(Error::UdpBind) } /// 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) { + let abort_handle = self.dns_server_task.abort_handle(); + let _abort_dns_server_task = on_drop(|| abort_handle.abort()); + while let Some(request) = self.rx.next().await { match request { ResolverMessage::SetConfig { @@ -369,7 +441,7 @@ impl LocalResolver { } => { log::debug!("Updating config: {new_config:?}"); - self.inner_resolver = Resolver::from(new_config); + self.update_config(new_config); flush_system_cache(); let _ = response_tx.send(()); } @@ -381,12 +453,37 @@ impl LocalResolver { } } } + } - if let Some((server_handle, done_rx)) = self.dns_server.take() { - server_handle.abort(); - let _ = done_rx.await; + /// Update the current DNS config. + fn update_config(&mut self, config: Config) { + match config { + Config::Blocking => self.blocking(), + Config::Forwarding { mut dns_servers } => { + // make sure not to accidentally forward queries to ourselves + dns_servers.retain(|addr| *addr != self.bound_to.ip()); + self.forwarding(dns_servers); + } } } + + /// Turn into a blocking resolver. + fn blocking(&mut self) { + self.inner_resolver = Resolver::Blocking; + } + + /// Turn into a forwarding resolver (forward DNS queries to [dns_servers]). + fn forwarding(&mut self, dns_servers: Vec<IpAddr>) { + let forward_server_config = + NameServerConfigGroup::from_ips_clear(&dns_servers, DNS_PORT, true); + + let forward_config = ResolverConfig::from_parts(None, vec![], forward_server_config); + let resolver_opts = ResolverOpts::default(); + + let resolver = TokioAsyncResolver::tokio(forward_config, resolver_opts); + + self.inner_resolver = Resolver::Forwarding(resolver); + } } /// Flush the DNS cache. @@ -535,26 +632,32 @@ mod test { config::{NameServerConfigGroup, ResolverConfig, ResolverOpts}, TokioAsyncResolver, }; - use std::{mem, net::UdpSocket, thread, time::Duration}; + use std::{mem, net::UdpSocket, sync::Mutex, thread, time::Duration}; + + /// Can't have multiple local resolvers running at the same time, as they will try to bind to + /// the same address and port. The tests below use this lock to run sequentially. + static LOCK: Mutex<()> = Mutex::new(()); async fn start_resolver() -> ResolverHandle { super::start_resolver().await.unwrap() } - fn get_test_resolver(port: u16) -> hickory_server::resolver::TokioAsyncResolver { + fn get_test_resolver(addr: SocketAddr) -> hickory_server::resolver::TokioAsyncResolver { let resolver_config = ResolverConfig::from_parts( None, vec![], - NameServerConfigGroup::from_ips_clear(&[Ipv4Addr::LOCALHOST.into()], port, true), + NameServerConfigGroup::from_ips_clear(&[addr.ip()], addr.port(), true), ); TokioAsyncResolver::tokio(resolver_config, ResolverOpts::default()) } - #[test] + #[test_log::test] fn test_successful_lookup() { + let _mutex = LOCK.lock().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap(); + let handle = rt.block_on(start_resolver()); - let test_resolver = get_test_resolver(handle.listening_port()); + let test_resolver = get_test_resolver(handle.listening_addr()); rt.block_on(async move { for domain in &*ALLOWED_DOMAINS { @@ -565,12 +668,13 @@ mod test { .expect("Resolution of domains failed"); } - #[test] + #[test_log::test] fn test_failed_lookup() { + let _mutex = LOCK.lock().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap(); let handle = rt.block_on(start_resolver()); - let test_resolver = get_test_resolver(handle.listening_port()); + let test_resolver = get_test_resolver(handle.listening_addr()); let captive_portal_domain = LowerName::from(Name::from_str("apple.com").unwrap()); let resolver_result = rt.block_on(async move { @@ -584,15 +688,15 @@ mod test { ) } - #[test] + #[test_log::test] fn test_shutdown() { + let _mutex = LOCK.lock().unwrap(); let rt = tokio::runtime::Runtime::new().unwrap(); let handle = rt.block_on(start_resolver()); - let port = handle.listening_port(); + let addr = handle.listening_addr(); 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"); + UdpSocket::bind(addr).expect("Failed to bind to a port that should have been removed"); } } diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index 05024554a3..3348f68603 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -6,8 +6,6 @@ use talpid_types::net::{AllowedClients, AllowedEndpoint, TunnelParameters}; use talpid_types::tunnel::{ErrorStateCause, FirewallPolicyError}; use talpid_types::{BoxedError, ErrorExt}; -#[cfg(target_os = "macos")] -use crate::dns::DnsConfig; use crate::dns::ResolvedDnsConfig; use crate::firewall::FirewallPolicy; #[cfg(target_os = "macos")] @@ -141,8 +139,6 @@ impl ConnectedState { dns_config: Self::resolve_dns(&self.metadata, shared_values), #[cfg(target_os = "macos")] redirect_interface, - #[cfg(target_os = "macos")] - dns_redirect_port: shared_values.filtering_resolver.listening_port(), } } @@ -166,11 +162,10 @@ impl ConnectedState { .set(&self.metadata.interface, dns_config) .map_err(BoxedError::new)?; - // On macOS, configure only the local DNS resolver #[cfg(target_os = "macos")] // We do not want to forward DNS queries to *our* local resolver if we do not run a local - // DNS resolver *or* if the DNS config points to a loopback address. - if dns_config.is_loopback() || !*LOCAL_DNS_RESOLVER { + // DNS resolver. + if !*LOCAL_DNS_RESOLVER { log::debug!("Not enabling local DNS resolver"); shared_values .dns_monitor @@ -185,15 +180,6 @@ impl ConnectedState { .filtering_resolver .enable_forward(dns_config.addresses().collect()), ); - // Set system DNS to our local DNS resolver - let system_dns = DnsConfig::default().resolve( - &[std::net::Ipv4Addr::LOCALHOST.into()], - shared_values.filtering_resolver.listening_port(), - ); - shared_values - .dns_monitor - .set("lo", system_dns) - .map_err(BoxedError::new)?; } Ok(()) @@ -207,9 +193,15 @@ impl ConnectedState { // On macOS, configure only the local DNS resolver #[cfg(target_os = "macos")] - shared_values - .runtime - .block_on(shared_values.filtering_resolver.disable_forward()); + if !*LOCAL_DNS_RESOLVER { + if let Err(error) = shared_values.dns_monitor.reset_before_interface_removal() { + log::error!("{}", error.display_chain_with_msg("Unable to reset DNS")); + } + } else { + shared_values + .runtime + .block_on(shared_values.filtering_resolver.disable_forward()); + } } fn reset_routes( diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 66534f6081..cdf324b5b1 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -59,8 +59,8 @@ impl ConnectingState { if *LOCAL_DNS_RESOLVER { // Set system DNS to our local DNS resolver let system_dns = DnsConfig::default().resolve( - &[std::net::Ipv4Addr::LOCALHOST.into()], - shared_values.filtering_resolver.listening_port(), + &[shared_values.filtering_resolver.listening_addr().ip()], + shared_values.filtering_resolver.listening_addr().port(), ); let _ = shared_values .dns_monitor @@ -200,8 +200,6 @@ impl ConnectingState { allowed_tunnel_traffic, #[cfg(target_os = "macos")] redirect_interface, - #[cfg(target_os = "macos")] - dns_redirect_port: shared_values.filtering_resolver.listening_port(), }; shared_values .firewall diff --git a/talpid-core/src/tunnel_state_machine/disconnected_state.rs b/talpid-core/src/tunnel_state_machine/disconnected_state.rs index de1d4822cb..8f96ff7b90 100644 --- a/talpid-core/src/tunnel_state_machine/disconnected_state.rs +++ b/talpid-core/src/tunnel_state_machine/disconnected_state.rs @@ -8,8 +8,6 @@ use crate::firewall::FirewallPolicy; use crate::{dns, tunnel_state_machine::ErrorState}; use futures::StreamExt; #[cfg(target_os = "macos")] -use std::net::Ipv4Addr; -#[cfg(target_os = "macos")] use talpid_types::tunnel::ErrorStateCause; use talpid_types::ErrorExt; @@ -80,8 +78,6 @@ impl DisconnectedState { let policy = FirewallPolicy::Blocked { allow_lan: shared_values.allow_lan, allowed_endpoint: Some(shared_values.allowed_endpoint.clone()), - #[cfg(target_os = "macos")] - dns_redirect_port: shared_values.filtering_resolver.listening_port(), }; shared_values.firewall.apply_policy(policy).map_err(|e| { @@ -149,8 +145,8 @@ impl DisconnectedState { shared_values.dns_monitor.set( "lo", dns::DnsConfig::default().resolve( - &[Ipv4Addr::LOCALHOST.into()], - shared_values.filtering_resolver.listening_port(), + &[shared_values.filtering_resolver.listening_addr().ip()], + shared_values.filtering_resolver.listening_addr().port(), ), ) } diff --git a/talpid-core/src/tunnel_state_machine/error_state.rs b/talpid-core/src/tunnel_state_machine/error_state.rs index 0cf392a0e5..ca280b9584 100644 --- a/talpid-core/src/tunnel_state_machine/error_state.rs +++ b/talpid-core/src/tunnel_state_machine/error_state.rs @@ -6,9 +6,9 @@ use super::{ use crate::dns::DnsConfig; #[cfg(not(target_os = "android"))] use crate::firewall::FirewallPolicy; -use futures::StreamExt; #[cfg(target_os = "macos")] -use std::net::Ipv4Addr; +use crate::resolver::LOCAL_DNS_RESOLVER; +use futures::StreamExt; use talpid_types::{ tunnel::{ErrorStateCause, FirewallPolicyError}, ErrorExt, @@ -38,8 +38,8 @@ impl ErrorState { if !block_reason.prevents_filtering_resolver() { // Set system DNS to our local DNS resolver let system_dns = DnsConfig::default().resolve( - &[Ipv4Addr::LOCALHOST.into()], - shared_values.filtering_resolver.listening_port(), + &[shared_values.filtering_resolver.listening_addr().ip()], + shared_values.filtering_resolver.listening_addr().port(), ); if let Err(err) = shared_values.dns_monitor.set("lo", system_dns) { log::error!( @@ -80,8 +80,6 @@ impl ErrorState { let policy = FirewallPolicy::Blocked { allow_lan: shared_values.allow_lan, allowed_endpoint: Some(shared_values.allowed_endpoint.clone()), - #[cfg(target_os = "macos")] - dns_redirect_port: shared_values.filtering_resolver.listening_port(), }; #[cfg(target_os = "linux")] @@ -189,13 +187,30 @@ impl TunnelState for ErrorState { if !connectivity.is_offline() && matches!(self.block_reason, ErrorStateCause::IsOffline) { + #[cfg(target_os = "macos")] + if !*LOCAL_DNS_RESOLVER { + // This is probably unnecessary, since DNS is already configured on the + // primary interface. + Self::reset_dns(shared_values); + } + + #[cfg(not(target_os = "macos"))] Self::reset_dns(shared_values); + NewState(ConnectingState::enter(shared_values, 0)) } else { SameState(self) } } Some(TunnelCommand::Connect) => { + #[cfg(target_os = "macos")] + if !*LOCAL_DNS_RESOLVER { + // This is probably unnecessary, since DNS is already configured on the + // primary interface. + Self::reset_dns(shared_values); + } + + #[cfg(not(target_os = "macos"))] Self::reset_dns(shared_values); NewState(ConnectingState::enter(shared_values, 0)) diff --git a/talpid-types/src/drop_guard.rs b/talpid-types/src/drop_guard.rs new file mode 100644 index 0000000000..1327bcf1f3 --- /dev/null +++ b/talpid-types/src/drop_guard.rs @@ -0,0 +1,39 @@ +/// Create a value that executes function `F` when dropped. +#[must_use = "Should be assigned to a variable and implicitly or explicitly dropped"] +pub fn on_drop<F: FnOnce()>(f: F) -> OnDrop<F> { + OnDrop(Some(f)) +} + +/// A type that executes a function when dropped. +/// +/// The default type of `F` is a boxed function. It's also [Send] in order to be useful with async. +/// If you need a less restrictive type for `F` you can specify it explicitly. +pub struct OnDrop<F: FnOnce() = Box<dyn FnOnce() + Send>>(Option<F>); + +impl<F: FnOnce()> OnDrop<F> { + /// Map the wrapped function into some other function. + pub fn map<F2: FnOnce()>(mut self, map: impl FnOnce(F) -> F2) -> OnDrop<F2> { + let f = self.0.take(); + OnDrop(f.map(map)) + } + + /// A drop guard that does nothing + pub fn noop() -> Self { + OnDrop(None) + } +} + +impl<F: FnOnce() + Send + 'static> OnDrop<F> { + /// Box the inner function to erase its type. + pub fn boxed(self) -> OnDrop { + self.map(|f| Box::new(f) as Box<_>) + } +} + +impl<F: FnOnce()> Drop for OnDrop<F> { + fn drop(&mut self) { + if let Some(f) = self.0.take() { + f() + } + } +} diff --git a/talpid-types/src/lib.rs b/talpid-types/src/lib.rs index 34581f074a..df6396ad10 100644 --- a/talpid-types/src/lib.rs +++ b/talpid-types/src/lib.rs @@ -9,5 +9,7 @@ pub mod cgroup; #[cfg(target_os = "windows")] pub mod split_tunnel; +pub mod drop_guard; + mod error; pub use error::*; diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs index 6144e78a0d..308d8c335b 100644 --- a/talpid-wireguard/src/lib.rs +++ b/talpid-wireguard/src/lib.rs @@ -882,15 +882,20 @@ impl WireguardMonitor { iface_name: &str, config: &'a Config, ) -> impl Iterator<Item = RequiredRoute> + 'a { + // e.g. utun4 let gateway_node = talpid_routing::Node::device(iface_name.to_string()); + + // e.g. route to 10.64.0.1 through utun4 let gateway_routes = std::iter::once(RequiredRoute::new( ipnetwork::Ipv4Network::from(config.ipv4_gateway).into(), gateway_node.clone(), )) + // same but ipv6 .chain(config.ipv6_gateway.map(|gateway| { RequiredRoute::new(ipnetwork::Ipv6Network::from(gateway).into(), gateway_node) })); + // e.g. utun4 and utun4 let (node_v4, node_v6) = Self::get_tunnel_nodes(iface_name, config); #[cfg(any(target_os = "linux", target_os = "macos"))] diff --git a/test/test-manager/src/tests/helpers.rs b/test/test-manager/src/tests/helpers.rs index 1ad7c9931a..97c2ded0f7 100644 --- a/test/test-manager/src/tests/helpers.rs +++ b/test/test-manager/src/tests/helpers.rs @@ -75,9 +75,16 @@ pub async fn install_app( rpc.install_app(get_package_desc(app_filename)).await?; // verify that daemon is running - if rpc.mullvad_daemon_get_status().await? != ServiceStatus::Running { - bail!(Error::DaemonNotRunning); - } + tokio::time::timeout(Duration::from_secs(5), async { + loop { + if rpc.mullvad_daemon_get_status().await? == ServiceStatus::Running { + return Ok::<_, Error>(()); + } + sleep(Duration::from_millis(100)).await; + } + }) + .await + .map_err(|_timeout| Error::DaemonNotRunning)??; // Set the log level to trace rpc.set_daemon_log_level(test_rpc::mullvad_daemon::Verbosity::Trace) diff --git a/wireguard-go-rs/Cargo.toml b/wireguard-go-rs/Cargo.toml index 8a7404cffd..415365e772 100644 --- a/wireguard-go-rs/Cargo.toml +++ b/wireguard-go-rs/Cargo.toml @@ -12,6 +12,8 @@ thiserror.workspace = true log.workspace = true zeroize = "1.8.1" +talpid-types.path = "../talpid-types" + # On platforms where maybenot and wireguard-go can be built statically (Linux and macOS) we use # this hack to include it. The hack is that we depend on this crate here even if neither # wireguard-go-rs nor its upstream dependants use it. diff --git a/wireguard-go-rs/src/lib.rs b/wireguard-go-rs/src/lib.rs index 37756a7afb..6ac5c928f3 100644 --- a/wireguard-go-rs/src/lib.rs +++ b/wireguard-go-rs/src/lib.rs @@ -13,13 +13,11 @@ use core::mem::MaybeUninit; use core::slice; #[cfg(target_os = "windows")] use std::ffi::CString; -use util::OnDrop; +use talpid_types::drop_guard::on_drop; #[cfg(target_os = "windows")] use windows_sys::Win32::NetworkManagement::Ndis::NET_LUID_LH; use zeroize::Zeroize; -mod util; - #[cfg(unix)] pub type Fd = std::os::unix::io::RawFd; @@ -223,7 +221,7 @@ impl Tunnel { let config_len = config.to_bytes().len(); // execute cleanup code on Drop to make sure that it happens even if `f` panics - let on_drop = OnDrop::new(|| { + let on_drop = on_drop(|| { { // SAFETY: // we checked for null, and wgGetConfig promises that this is a valid cstr. diff --git a/wireguard-go-rs/src/util.rs b/wireguard-go-rs/src/util.rs deleted file mode 100644 index 4c2df2f9bb..0000000000 --- a/wireguard-go-rs/src/util.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub struct OnDrop<F: FnOnce()>(Option<F>); - -impl<F: FnOnce()> OnDrop<F> { - pub fn new(f: F) -> Self { - OnDrop(Some(f)) - } -} - -impl<F: FnOnce()> Drop for OnDrop<F> { - fn drop(&mut self) { - if let Some(f) = self.0.take() { - f() - } - } -} |
