summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJoakim Hulthe <joakim.hulthe@mullvad.net>2025-05-14 21:00:52 +0200
committerJoakim Hulthe <joakim.hulthe@mullvad.net>2025-05-14 21:00:52 +0200
commite191432724e7c2f07953704a8fc875e4ccf9f55e (patch)
tree388d04d931e5706adaa35614a7347299f249718b
parentd17158a3384e3fd2aa7f2e06ff6aa5bfb728eb66 (diff)
parent49f0eea29f68f01dd52b7b783a1dabd88fc964c8 (diff)
downloadmullvadvpn-e191432724e7c2f07953704a8fc875e4ccf9f55e.tar.xz
mullvadvpn-e191432724e7c2f07953704a8fc875e4ccf9f55e.zip
Merge branch 'investigate-dns-issues-tb-3-days-des-2076'
-rw-r--r--CHANGELOG.md4
-rw-r--r--Cargo.lock128
-rw-r--r--README.md2
-rw-r--r--talpid-core/Cargo.toml1
-rw-r--r--talpid-core/src/dns/macos.rs9
-rw-r--r--talpid-core/src/firewall/macos.rs140
-rw-r--r--talpid-core/src/firewall/mod.rs26
-rw-r--r--talpid-core/src/resolver.rs338
-rw-r--r--talpid-core/src/tunnel_state_machine/connected_state.rs30
-rw-r--r--talpid-core/src/tunnel_state_machine/connecting_state.rs6
-rw-r--r--talpid-core/src/tunnel_state_machine/disconnected_state.rs8
-rw-r--r--talpid-core/src/tunnel_state_machine/error_state.rs27
-rw-r--r--talpid-types/src/drop_guard.rs39
-rw-r--r--talpid-types/src/lib.rs2
-rw-r--r--talpid-wireguard/src/lib.rs5
-rw-r--r--test/test-manager/src/tests/helpers.rs13
-rw-r--r--wireguard-go-rs/Cargo.toml2
-rw-r--r--wireguard-go-rs/src/lib.rs6
-rw-r--r--wireguard-go-rs/src/util.rs15
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",
diff --git a/README.md b/README.md
index 1dca064ed3..13b940e192 100644
--- a/README.md
+++ b/README.md
@@ -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()
- }
- }
-}