mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
fix(connlib): retain order of system/upstream DNS servers (#10773)
Right now, connlib hands out a `BiMap` of sentinel IPs <> upstream servers whenever it emits a `TunInterfaceUpdated` event. This `BiMap` internally uses two `HashMap`s. The iteration order of `HashMap`s is non-deterministic and therefore, we lose the order in which the upstream / system resolvers have been passed to us originally. To prevent that, we now emit a dedicated `DnsMapping` type that does not expose its internal data structure but only getters for retrieving the sentinel and upstream servers. Internally, it uses a `Vec` to store this mapping and thus retains the original order. This is asserted as part of our proptests by comparing the resulting `Vec`s. This fix is preceded by a few refactorings that encapsulate the code for creating and updating this DNS mapping. Resolves: #8439
This commit is contained in:
@@ -379,9 +379,10 @@ impl Session {
|
||||
pub async fn next_event(&self) -> Option<Event> {
|
||||
match self.events.lock().await.next().await? {
|
||||
client_shared::Event::TunInterfaceUpdated(config) => {
|
||||
let dns: Vec<String> = config
|
||||
let dns = config
|
||||
.dns_by_sentinel
|
||||
.left_values()
|
||||
.sentinel_ips()
|
||||
.into_iter()
|
||||
.map(|ip| ip.to_string())
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
mod dns_cache;
|
||||
pub(crate) mod dns_config;
|
||||
mod dns_resource_nat;
|
||||
mod gateway_on_client;
|
||||
mod pending_tun_update;
|
||||
mod resource;
|
||||
|
||||
use crate::client::dns_config::DnsConfig;
|
||||
pub(crate) use crate::client::gateway_on_client::GatewayOnClient;
|
||||
use crate::client::pending_tun_update::PendingTunUpdate;
|
||||
use boringtun::x25519;
|
||||
@@ -20,13 +22,12 @@ use secrecy::ExposeSecret as _;
|
||||
use crate::client::dns_cache::DnsCache;
|
||||
use crate::dns::{DnsResourceRecord, StubResolver};
|
||||
use crate::expiring_map::{self, ExpiringMap};
|
||||
use crate::messages::{DnsServer, Interface as InterfaceConfig, IpDnsServer};
|
||||
use crate::messages::Interface as InterfaceConfig;
|
||||
use crate::messages::{IceCredentials, SecretKey};
|
||||
use crate::peer_store::PeerStore;
|
||||
use crate::unique_packet_buffer::UniquePacketBuffer;
|
||||
use crate::{IPV4_TUNNEL, IPV6_TUNNEL, IpConfig, TunConfig, dns, is_peer, p2p_control};
|
||||
use anyhow::Context;
|
||||
use bimap::BiMap;
|
||||
use connlib_model::{
|
||||
GatewayId, IceCandidate, PublicKey, RelayId, ResourceId, ResourceStatus, ResourceView,
|
||||
};
|
||||
@@ -40,7 +41,7 @@ use itertools::Itertools;
|
||||
use crate::ClientEvent;
|
||||
use lru::LruCache;
|
||||
use snownet::{ClientNode, NoTurnServers, RelaySocket, Transmit};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::ControlFlow;
|
||||
@@ -118,15 +119,9 @@ pub struct ClientState {
|
||||
/// All resources indexed by their ID.
|
||||
resources_by_id: BTreeMap<ResourceId, Resource>,
|
||||
|
||||
/// The DNS resolvers configured on the system outside of connlib.
|
||||
system_resolvers: Vec<IpAddr>,
|
||||
/// The DNS resolvers configured in the portal.
|
||||
///
|
||||
/// Has priority over system-configured DNS servers.
|
||||
upstream_dns: Vec<DnsServer>,
|
||||
/// Manages the DNS configuration.
|
||||
dns_config: DnsConfig,
|
||||
|
||||
/// Maps from connlib-assigned IP of a DNS server back to the originally configured system DNS resolver.
|
||||
dns_mapping: BiMap<IpAddr, DnsServer>,
|
||||
/// UDP DNS queries that had their destination IP mangled to redirect them to another DNS resolver through the tunnel.
|
||||
udp_dns_sockets_by_upstream_and_query_id: ExpiringMap<(SocketAddr, u16), SocketAddr>,
|
||||
/// Manages internal dns records and emits forwarding event when not internally handled
|
||||
@@ -209,12 +204,11 @@ impl ClientState {
|
||||
active_cidr_resources: IpNetworkTable::new(),
|
||||
resources_by_id: Default::default(),
|
||||
peers: Default::default(),
|
||||
dns_mapping: Default::default(),
|
||||
dns_config: Default::default(),
|
||||
buffered_events: Default::default(),
|
||||
tun_config: Default::default(),
|
||||
buffered_packets: Default::default(),
|
||||
node: ClientNode::new(seed, now),
|
||||
system_resolvers: Default::default(),
|
||||
sites_status: Default::default(),
|
||||
gateways_site: Default::default(),
|
||||
udp_dns_sockets_by_upstream_and_query_id: Default::default(),
|
||||
@@ -223,7 +217,6 @@ impl ClientState {
|
||||
buffered_transmits: Default::default(),
|
||||
is_internet_resource_active,
|
||||
recently_connected_gateways: LruCache::new(MAX_REMEMBERED_GATEWAYS),
|
||||
upstream_dns: Default::default(),
|
||||
buffered_dns_queries: Default::default(),
|
||||
tcp_dns_client: dns_over_tcp::Client::new(now, seed),
|
||||
tcp_dns_server: dns_over_tcp::Server::new(now),
|
||||
@@ -629,9 +622,10 @@ impl ClientState {
|
||||
dst: SocketAddr,
|
||||
message: dns_types::Response,
|
||||
) -> anyhow::Result<()> {
|
||||
let saddr = *self
|
||||
.dns_mapping
|
||||
.get_by_right(&DnsServer::from(from))
|
||||
let saddr = self
|
||||
.dns_config
|
||||
.mapping()
|
||||
.sentinel_by_upstream(from)
|
||||
.context("Unknown DNS server")?;
|
||||
|
||||
let ip_packet = ip_packet::make::udp_packet(
|
||||
@@ -807,15 +801,11 @@ impl ClientState {
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
||||
fn is_upstream_set_by_the_portal(&self) -> bool {
|
||||
!self.upstream_dns.is_empty()
|
||||
}
|
||||
|
||||
/// For DNS queries to IPs that are a CIDR resources we want to mangle and forward to the gateway that handles that resource.
|
||||
///
|
||||
/// We only want to do this if the upstream DNS server is set by the portal, otherwise, the server might be a local IP.
|
||||
fn should_forward_dns_query_to_gateway(&self, dns_server: IpAddr) -> bool {
|
||||
if !self.is_upstream_set_by_the_portal() {
|
||||
if !self.dns_config.has_custom_upstream() {
|
||||
return false;
|
||||
}
|
||||
if self.active_internet_resource().is_some() {
|
||||
@@ -836,7 +826,7 @@ impl ClientState {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
|
||||
let Some(upstream) = self.dns_mapping.get_by_left(&dst).map(|s| s.address()) else {
|
||||
let Some(upstream) = self.dns_config.mapping().upstream_by_sentinel(dst) else {
|
||||
return ControlFlow::Continue(packet); // Not for our DNS resolver.
|
||||
};
|
||||
|
||||
@@ -911,10 +901,6 @@ impl ClientState {
|
||||
self.resources_gateways.get(resource).copied()
|
||||
}
|
||||
|
||||
fn set_dns_mapping(&mut self, new_mapping: BiMap<IpAddr, DnsServer>) {
|
||||
self.dns_mapping = new_mapping;
|
||||
}
|
||||
|
||||
fn initialise_tcp_dns_client(&mut self) {
|
||||
let Some(tun_config) = self.tun_config.as_ref() else {
|
||||
return;
|
||||
@@ -927,9 +913,11 @@ impl ClientState {
|
||||
|
||||
fn initialise_tcp_dns_server(&mut self) {
|
||||
let sentinel_sockets = self
|
||||
.dns_mapping
|
||||
.left_values()
|
||||
.map(|ip| SocketAddr::new(*ip, DNS_PORT))
|
||||
.dns_config
|
||||
.mapping()
|
||||
.sentinel_ips()
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, DNS_PORT))
|
||||
.collect();
|
||||
|
||||
self.tcp_dns_server
|
||||
@@ -966,10 +954,6 @@ impl ClientState {
|
||||
self.maybe_update_tun_routes();
|
||||
}
|
||||
|
||||
pub fn dns_mapping(&self) -> BiMap<IpAddr, DnsServer> {
|
||||
self.dns_mapping.clone()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(gateway = %disconnected_gateway))]
|
||||
fn cleanup_connected_gateway(&mut self, disconnected_gateway: &GatewayId) {
|
||||
self.update_site_status_by_gateway(disconnected_gateway, ResourceStatus::Unknown);
|
||||
@@ -1046,16 +1030,38 @@ impl ClientState {
|
||||
}
|
||||
|
||||
pub fn update_system_resolvers(&mut self, new_dns: Vec<IpAddr>) {
|
||||
tracing::debug!(servers = ?new_dns, "Received system-defined DNS servers");
|
||||
let changed = self.dns_config.update_system_resolvers(new_dns);
|
||||
|
||||
self.system_resolvers = new_dns;
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
|
||||
self.update_dns_mapping()
|
||||
self.dns_cache.flush("DNS servers changed");
|
||||
|
||||
let Some(config) = self.tun_config.clone() else {
|
||||
tracing::debug!("Unable to update DNS servers without interface configuration");
|
||||
return;
|
||||
};
|
||||
|
||||
let dns_by_sentinel = self.dns_config.mapping();
|
||||
|
||||
self.maybe_update_tun_config(TunConfig {
|
||||
dns_by_sentinel,
|
||||
..config
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_interface_config(&mut self, config: InterfaceConfig) {
|
||||
tracing::trace!(upstream_dns = ?config.upstream_dns, search_domain = ?config.search_domain, ipv4 = %config.ipv4, ipv6 = %config.ipv6, "Received interface configuration from portal");
|
||||
|
||||
let changed = self
|
||||
.dns_config
|
||||
.update_upstream_resolvers(config.upstream_dns);
|
||||
|
||||
if changed {
|
||||
self.dns_cache.flush("DNS servers changed");
|
||||
}
|
||||
|
||||
// Create a new `TunConfig` by patching the corresponding fields of the existing one.
|
||||
let new_tun_config = self
|
||||
.tun_config
|
||||
@@ -1065,7 +1071,7 @@ impl ClientState {
|
||||
v4: config.ipv4,
|
||||
v6: config.ipv6,
|
||||
},
|
||||
dns_by_sentinel: existing.dns_by_sentinel.clone(),
|
||||
dns_by_sentinel: self.dns_config.mapping(),
|
||||
search_domain: config.search_domain.clone(),
|
||||
ipv4_routes: existing.ipv4_routes.clone(),
|
||||
ipv6_routes: existing.ipv6_routes.clone(),
|
||||
@@ -1081,7 +1087,7 @@ impl ClientState {
|
||||
v4: config.ipv4,
|
||||
v6: config.ipv6,
|
||||
},
|
||||
dns_by_sentinel: Default::default(),
|
||||
dns_by_sentinel: self.dns_config.mapping(),
|
||||
search_domain: config.search_domain.clone(),
|
||||
ipv4_routes,
|
||||
ipv6_routes,
|
||||
@@ -1090,9 +1096,6 @@ impl ClientState {
|
||||
|
||||
// Apply the new `TunConfig` if it differs from the existing one.
|
||||
self.maybe_update_tun_config(new_tun_config);
|
||||
|
||||
self.upstream_dns = config.upstream_dns;
|
||||
self.update_dns_mapping();
|
||||
}
|
||||
|
||||
pub fn poll_packets(&mut self) -> Option<IpPacket> {
|
||||
@@ -1408,11 +1411,14 @@ impl ClientState {
|
||||
fn handle_tcp_dns_query(&mut self, query: dns_over_tcp::Query, now: Instant) {
|
||||
let query_id = query.message.id();
|
||||
|
||||
let Some(upstream) = self.dns_mapping.get_by_left(&query.local.ip()) else {
|
||||
let Some(server) = self
|
||||
.dns_config
|
||||
.mapping()
|
||||
.upstream_by_sentinel(query.local.ip())
|
||||
else {
|
||||
// This is highly-unlikely but might be possible if our DNS mapping changes whilst the TCP DNS server is processing a request.
|
||||
return;
|
||||
};
|
||||
let server = upstream.address();
|
||||
|
||||
if let Some(response) = self.dns_cache.try_answer(&query.message, now) {
|
||||
unwrap_or_debug!(
|
||||
@@ -1899,55 +1905,6 @@ impl ClientState {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_dns_mapping(&mut self) {
|
||||
let Some(config) = self.tun_config.clone() else {
|
||||
// For the Tauri clients this can happen because it's called immediately after phoenix_channel's connect, before on_set_interface_config
|
||||
tracing::debug!("Unable to update DNS servers without interface configuration");
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let effective_dns_servers =
|
||||
effective_dns_servers(self.upstream_dns.clone(), self.system_resolvers.clone());
|
||||
|
||||
if HashSet::<&DnsServer>::from_iter(effective_dns_servers.iter())
|
||||
== HashSet::from_iter(self.dns_mapping.right_values())
|
||||
{
|
||||
tracing::debug!(servers = ?effective_dns_servers, "Effective DNS servers are unchanged");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let dns_mapping = sentinel_dns_mapping(
|
||||
&effective_dns_servers,
|
||||
self.dns_mapping()
|
||||
.left_values()
|
||||
.copied()
|
||||
.map(Into::into)
|
||||
.collect_vec(),
|
||||
);
|
||||
|
||||
let (ipv4_routes, ipv6_routes) = self.routes().partition_map(|route| match route {
|
||||
IpNetwork::V4(v4) => itertools::Either::Left(v4),
|
||||
IpNetwork::V6(v6) => itertools::Either::Right(v6),
|
||||
});
|
||||
|
||||
let new_tun_config = TunConfig {
|
||||
ip: config.ip,
|
||||
dns_by_sentinel: dns_mapping
|
||||
.iter()
|
||||
.map(|(sentinel_dns, effective_dns)| (*sentinel_dns, effective_dns.address()))
|
||||
.collect::<BiMap<_, _>>(),
|
||||
search_domain: config.search_domain,
|
||||
ipv4_routes,
|
||||
ipv6_routes,
|
||||
};
|
||||
|
||||
self.set_dns_mapping(dns_mapping);
|
||||
self.maybe_update_tun_config(new_tun_config);
|
||||
self.dns_cache.flush("DNS servers changed");
|
||||
}
|
||||
|
||||
pub fn update_relays(
|
||||
&mut self,
|
||||
to_remove: BTreeSet<RelayId>,
|
||||
@@ -1996,61 +1953,6 @@ fn peer_by_resource_mut<'p>(
|
||||
Some(peer)
|
||||
}
|
||||
|
||||
fn effective_dns_servers(
|
||||
upstream_dns: Vec<DnsServer>,
|
||||
default_resolvers: Vec<IpAddr>,
|
||||
) -> Vec<DnsServer> {
|
||||
let mut upstream_dns = upstream_dns.into_iter().filter_map(not_sentinel).peekable();
|
||||
if upstream_dns.peek().is_some() {
|
||||
return upstream_dns.collect();
|
||||
}
|
||||
|
||||
let mut dns_servers = default_resolvers
|
||||
.into_iter()
|
||||
.map(|ip| {
|
||||
DnsServer::IpPort(IpDnsServer {
|
||||
address: (ip, DNS_PORT).into(),
|
||||
})
|
||||
})
|
||||
.filter_map(not_sentinel)
|
||||
.peekable();
|
||||
|
||||
if dns_servers.peek().is_none() {
|
||||
tracing::info!(
|
||||
"No system default DNS servers available! Can't initialize resolver. DNS resources won't work."
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
dns_servers.collect()
|
||||
}
|
||||
|
||||
fn not_sentinel(srv: DnsServer) -> Option<DnsServer> {
|
||||
let is_v4_dns = IpNetwork::V4(DNS_SENTINELS_V4).contains(srv.ip());
|
||||
let is_v6_dns = IpNetwork::V6(DNS_SENTINELS_V6).contains(srv.ip());
|
||||
|
||||
(!is_v4_dns && !is_v6_dns).then_some(srv)
|
||||
}
|
||||
|
||||
fn sentinel_dns_mapping(
|
||||
dns: &[DnsServer],
|
||||
old_sentinels: Vec<IpNetwork>,
|
||||
) -> BiMap<IpAddr, DnsServer> {
|
||||
let mut ip_provider = IpProvider::for_stub_dns_servers(old_sentinels);
|
||||
|
||||
dns.iter()
|
||||
.cloned()
|
||||
.map(|i| {
|
||||
(
|
||||
ip_provider
|
||||
.get_proxy_ip_for(&i.ip())
|
||||
.expect("We only support up to 256 IPv4 DNS servers and 256 IPv6 DNS servers"),
|
||||
i,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compares the given [`IpAddr`] against a static set of ignored IPs that are definitely not resources.
|
||||
fn is_definitely_not_a_resource(ip: IpAddr) -> bool {
|
||||
/// Source: https://en.wikipedia.org/wiki/Multicast_address#Notable_IPv4_multicast_addresses
|
||||
@@ -2172,8 +2074,12 @@ impl IpProvider {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_stub_dns_servers(exclusions: Vec<IpNetwork>) -> Self {
|
||||
IpProvider::new(DNS_SENTINELS_V4, DNS_SENTINELS_V6, exclusions)
|
||||
pub fn for_stub_dns_servers(old_servers: Vec<IpAddr>) -> Self {
|
||||
IpProvider::new(
|
||||
DNS_SENTINELS_V4,
|
||||
DNS_SENTINELS_V6,
|
||||
old_servers.into_iter().map(IpNetwork::from).collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn new(ipv4: Ipv4Network, ipv6: Ipv6Network, exclusions: Vec<IpNetwork>) -> Self {
|
||||
@@ -2229,66 +2135,12 @@ mod tests {
|
||||
assert!(is_definitely_not_a_resource(ip("ff02::2")))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sentinel_dns_works() {
|
||||
let servers = dns_list();
|
||||
let sentinel_dns = sentinel_dns_mapping(&servers, vec![]);
|
||||
|
||||
for server in servers {
|
||||
assert!(
|
||||
sentinel_dns
|
||||
.get_by_right(&server)
|
||||
.is_some_and(|s| sentinel_ranges().iter().any(|e| e.contains(*s)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sentinel_dns_excludes_old_ones() {
|
||||
let servers = dns_list();
|
||||
let sentinel_dns_old = sentinel_dns_mapping(&servers, vec![]);
|
||||
let sentinel_dns_new = sentinel_dns_mapping(
|
||||
&servers,
|
||||
sentinel_dns_old
|
||||
.left_values()
|
||||
.copied()
|
||||
.map(Into::into)
|
||||
.collect_vec(),
|
||||
);
|
||||
|
||||
assert!(
|
||||
HashSet::<&IpAddr>::from_iter(sentinel_dns_old.left_values())
|
||||
.is_disjoint(&HashSet::from_iter(sentinel_dns_new.left_values()))
|
||||
)
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
pub fn for_test() -> ClientState {
|
||||
ClientState::new(rand::random(), Default::default(), false, Instant::now())
|
||||
}
|
||||
}
|
||||
|
||||
fn sentinel_ranges() -> Vec<IpNetwork> {
|
||||
vec![
|
||||
IpNetwork::V4(DNS_SENTINELS_V4),
|
||||
IpNetwork::V6(DNS_SENTINELS_V6),
|
||||
]
|
||||
}
|
||||
|
||||
fn dns_list() -> Vec<DnsServer> {
|
||||
vec![
|
||||
dns("1.1.1.1:53"),
|
||||
dns("1.0.0.1:53"),
|
||||
dns("[2606:4700:4700::1111]:53"),
|
||||
]
|
||||
}
|
||||
|
||||
fn dns(address: &str) -> DnsServer {
|
||||
DnsServer::IpPort(IpDnsServer {
|
||||
address: address.parse().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
fn ip(addr: &str) -> IpAddr {
|
||||
addr.parse().unwrap()
|
||||
}
|
||||
@@ -2296,6 +2148,8 @@ mod tests {
|
||||
|
||||
#[cfg(all(test, feature = "proptest"))]
|
||||
mod proptests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
use crate::proptest::*;
|
||||
use connlib_model::ResourceView;
|
||||
|
||||
211
rust/connlib/tunnel/src/client/dns_config.rs
Normal file
211
rust/connlib/tunnel/src/client/dns_config.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
|
||||
use ip_network::IpNetwork;
|
||||
|
||||
use crate::{
|
||||
client::{DNS_SENTINELS_V4, DNS_SENTINELS_V6, IpProvider},
|
||||
dns::DNS_PORT,
|
||||
messages::DnsServer,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct DnsConfig {
|
||||
/// The DNS resolvers configured on the system outside of connlib.
|
||||
system_resolvers: Vec<IpAddr>,
|
||||
/// The DNS resolvers configured in the portal.
|
||||
///
|
||||
/// Has priority over system-configured DNS servers.
|
||||
upstream_dns: Vec<SocketAddr>,
|
||||
|
||||
/// Maps from connlib-assigned IP of a DNS server back to the originally configured system DNS resolver.
|
||||
mapping: DnsMapping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct DnsMapping {
|
||||
inner: Vec<(IpAddr, SocketAddr)>,
|
||||
}
|
||||
|
||||
impl DnsMapping {
|
||||
pub fn sentinel_ips(&self) -> Vec<IpAddr> {
|
||||
self.inner.iter().map(|(ip, _)| ip).copied().collect()
|
||||
}
|
||||
|
||||
pub fn upstream_sockets(&self) -> Vec<SocketAddr> {
|
||||
self.inner
|
||||
.iter()
|
||||
.map(|(_, socket)| socket)
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Implementation note:
|
||||
//
|
||||
// These functions perform linear search instead of an O(1) map lookup.
|
||||
// Most users will only have a handful of DNS servers (like 1-3).
|
||||
// For such small numbers, linear search is usually more efficient.
|
||||
// Most importantly, it is much easier for us to retain the ordering of the DNS servers if we don't use a map.
|
||||
|
||||
pub(crate) fn sentinel_by_upstream(&self, upstream: SocketAddr) -> Option<IpAddr> {
|
||||
self.inner
|
||||
.iter()
|
||||
.find_map(|(sentinel, candidate)| (candidate == &upstream).then_some(*sentinel))
|
||||
}
|
||||
|
||||
pub(crate) fn upstream_by_sentinel(&self, sentinel: IpAddr) -> Option<SocketAddr> {
|
||||
self.inner
|
||||
.iter()
|
||||
.find_map(|(candidate, upstream)| (candidate == &sentinel).then_some(*upstream))
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsConfig {
|
||||
#[must_use = "Check if the DNS mapping has changed"]
|
||||
pub(crate) fn update_system_resolvers(&mut self, servers: Vec<IpAddr>) -> bool {
|
||||
tracing::debug!(?servers, "Received system-defined DNS servers");
|
||||
|
||||
self.system_resolvers = servers;
|
||||
|
||||
self.update_dns_mapping()
|
||||
}
|
||||
|
||||
#[must_use = "Check if the DNS mapping has changed"]
|
||||
pub(crate) fn update_upstream_resolvers(&mut self, servers: Vec<DnsServer>) -> bool {
|
||||
tracing::debug!(?servers, "Received upstream-defined DNS servers");
|
||||
|
||||
self.upstream_dns = servers.into_iter().map(|s| s.address()).collect();
|
||||
|
||||
self.update_dns_mapping()
|
||||
}
|
||||
|
||||
pub(crate) fn has_custom_upstream(&self) -> bool {
|
||||
!self.upstream_dns.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn mapping(&mut self) -> DnsMapping {
|
||||
self.mapping.clone()
|
||||
}
|
||||
|
||||
fn update_dns_mapping(&mut self) -> bool {
|
||||
let effective_dns_servers =
|
||||
effective_dns_servers(self.upstream_dns.clone(), self.system_resolvers.clone());
|
||||
|
||||
if HashSet::<SocketAddr>::from_iter(effective_dns_servers.clone())
|
||||
== HashSet::from_iter(self.mapping.upstream_sockets())
|
||||
{
|
||||
tracing::debug!(servers = ?effective_dns_servers, "Effective DNS servers are unchanged");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self.mapping = sentinel_dns_mapping(&effective_dns_servers, self.mapping.sentinel_ips());
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_dns_servers(
|
||||
upstream_dns: Vec<SocketAddr>,
|
||||
default_resolvers: Vec<IpAddr>,
|
||||
) -> Vec<SocketAddr> {
|
||||
let mut upstream_dns = upstream_dns.into_iter().filter_map(not_sentinel).peekable();
|
||||
if upstream_dns.peek().is_some() {
|
||||
return upstream_dns.collect();
|
||||
}
|
||||
|
||||
let mut dns_servers = default_resolvers
|
||||
.into_iter()
|
||||
.map(|ip| SocketAddr::new(ip, DNS_PORT))
|
||||
.filter_map(not_sentinel)
|
||||
.peekable();
|
||||
|
||||
if dns_servers.peek().is_none() {
|
||||
tracing::info!(
|
||||
"No system default DNS servers available! Can't initialize resolver. DNS resources won't work."
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
dns_servers.collect()
|
||||
}
|
||||
|
||||
fn sentinel_dns_mapping(dns: &[SocketAddr], old_sentinels: Vec<IpAddr>) -> DnsMapping {
|
||||
let mut ip_provider = IpProvider::for_stub_dns_servers(old_sentinels);
|
||||
|
||||
let mapping = dns
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|i| {
|
||||
(
|
||||
ip_provider
|
||||
.get_proxy_ip_for(&i.ip())
|
||||
.expect("We only support up to 256 IPv4 DNS servers and 256 IPv6 DNS servers"),
|
||||
i,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
DnsMapping { inner: mapping }
|
||||
}
|
||||
|
||||
fn not_sentinel(srv: SocketAddr) -> Option<SocketAddr> {
|
||||
let is_v4_dns = IpNetwork::V4(DNS_SENTINELS_V4).contains(srv.ip());
|
||||
let is_v6_dns = IpNetwork::V6(DNS_SENTINELS_V6).contains(srv.ip());
|
||||
|
||||
(!is_v4_dns && !is_v6_dns).then_some(srv)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sentinel_dns_works() {
|
||||
let servers = dns_list();
|
||||
let sentinel_dns = sentinel_dns_mapping(&servers, vec![]);
|
||||
|
||||
for server in servers {
|
||||
assert!(
|
||||
sentinel_dns
|
||||
.sentinel_by_upstream(server)
|
||||
.is_some_and(|ip| sentinel_ranges().iter().any(|e| e.contains(ip)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sentinel_dns_excludes_old_ones() {
|
||||
let servers = dns_list();
|
||||
let sentinel_dns_old = sentinel_dns_mapping(&servers, vec![]);
|
||||
let sentinel_dns_new = sentinel_dns_mapping(&servers, sentinel_dns_old.sentinel_ips());
|
||||
|
||||
assert!(
|
||||
HashSet::<IpAddr>::from_iter(sentinel_dns_old.sentinel_ips())
|
||||
.is_disjoint(&HashSet::from_iter(sentinel_dns_new.sentinel_ips()))
|
||||
)
|
||||
}
|
||||
|
||||
fn sentinel_ranges() -> Vec<IpNetwork> {
|
||||
vec![
|
||||
IpNetwork::V4(DNS_SENTINELS_V4),
|
||||
IpNetwork::V6(DNS_SENTINELS_V6),
|
||||
]
|
||||
}
|
||||
|
||||
fn dns_list() -> Vec<SocketAddr> {
|
||||
vec![
|
||||
dns("1.1.1.1:53"),
|
||||
dns("1.0.0.1:53"),
|
||||
dns("[2606:4700:4700::1111]:53"),
|
||||
]
|
||||
}
|
||||
|
||||
fn dns(address: &str) -> SocketAddr {
|
||||
address.parse().unwrap()
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use bimap::BiMap;
|
||||
use chrono::Utc;
|
||||
use connlib_model::{ClientId, GatewayId, IceCandidate, PublicKey, ResourceId, ResourceView};
|
||||
use dns_types::DomainName;
|
||||
@@ -69,6 +68,7 @@ pub type GatewayTunnel = Tunnel<GatewayState>;
|
||||
pub type ClientTunnel = Tunnel<ClientState>;
|
||||
|
||||
pub use client::ClientState;
|
||||
pub use client::dns_config::DnsMapping;
|
||||
pub use dns::DnsResourceRecord;
|
||||
pub use gateway::{DnsResourceNatEntry, GatewayState, ResolveDnsRequest};
|
||||
pub use sockets::UdpSocketThreadStopped;
|
||||
@@ -550,7 +550,7 @@ pub struct TunConfig {
|
||||
/// - The "right" values are the effective DNS servers.
|
||||
/// If upstream DNS servers are configured (in the portal), we will use those.
|
||||
/// Otherwise, we will use the DNS servers configured on the system.
|
||||
pub dns_by_sentinel: BiMap<IpAddr, SocketAddr>,
|
||||
pub dns_by_sentinel: DnsMapping,
|
||||
pub search_domain: Option<DomainName>,
|
||||
|
||||
#[debug("{}", DisplayBTreeSet(ipv4_routes))]
|
||||
@@ -559,12 +559,6 @@ pub struct TunConfig {
|
||||
pub ipv6_routes: BTreeSet<Ipv6Network>,
|
||||
}
|
||||
|
||||
impl TunConfig {
|
||||
pub fn dns_sentinel_ips(&self) -> Vec<IpAddr> {
|
||||
self.dns_by_sentinel.left_values().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IpConfig {
|
||||
pub v4: Ipv4Addr,
|
||||
|
||||
@@ -7,12 +7,11 @@ use super::{
|
||||
strategies::latency,
|
||||
transition::{DPort, Destination, DnsQuery, DnsTransport, Identifier, SPort, Seq},
|
||||
};
|
||||
use crate::{ClientState, DnsResourceRecord, proptest::*};
|
||||
use crate::{ClientState, DnsMapping, DnsResourceRecord, proptest::*};
|
||||
use crate::{
|
||||
client::{CidrResource, DnsResource, InternetResource, Resource},
|
||||
messages::{DnsServer, Interface},
|
||||
};
|
||||
use bimap::BiMap;
|
||||
use connlib_model::{ClientId, GatewayId, RelayId, ResourceId, ResourceStatus, Site, SiteId};
|
||||
use dns_types::{DomainName, Query, RecordData, RecordType};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
@@ -47,7 +46,7 @@ pub(crate) struct SimClient {
|
||||
pub(crate) dns_resource_record_cache: BTreeSet<DnsResourceRecord>,
|
||||
|
||||
/// Bi-directional mapping between connlib's sentinel DNS IPs and the effective DNS servers.
|
||||
dns_by_sentinel: BiMap<IpAddr, SocketAddr>,
|
||||
dns_by_sentinel: DnsMapping,
|
||||
|
||||
pub(crate) ipv4_routes: BTreeSet<Ipv4Network>,
|
||||
pub(crate) ipv6_routes: BTreeSet<Ipv6Network>,
|
||||
@@ -123,26 +122,26 @@ impl SimClient {
|
||||
);
|
||||
|
||||
self.search_domain = None;
|
||||
self.dns_by_sentinel.clear();
|
||||
self.dns_by_sentinel = DnsMapping::default();
|
||||
self.ipv4_routes.clear();
|
||||
self.ipv6_routes.clear();
|
||||
}
|
||||
|
||||
/// Returns the _effective_ DNS servers that connlib is using.
|
||||
pub(crate) fn effective_dns_servers(&self) -> BTreeSet<SocketAddr> {
|
||||
self.dns_by_sentinel.right_values().copied().collect()
|
||||
pub(crate) fn effective_dns_servers(&self) -> Vec<SocketAddr> {
|
||||
self.dns_by_sentinel.upstream_sockets()
|
||||
}
|
||||
|
||||
pub(crate) fn effective_search_domain(&self) -> Option<DomainName> {
|
||||
self.search_domain.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn set_new_dns_servers(&mut self, mapping: BiMap<IpAddr, SocketAddr>) {
|
||||
pub(crate) fn set_new_dns_servers(&mut self, mapping: DnsMapping) {
|
||||
self.dns_by_sentinel = mapping;
|
||||
self.tcp_dns_client.reset();
|
||||
}
|
||||
|
||||
pub(crate) fn dns_mapping(&self) -> &BiMap<IpAddr, SocketAddr> {
|
||||
pub(crate) fn dns_mapping(&self) -> &DnsMapping {
|
||||
&self.dns_by_sentinel
|
||||
}
|
||||
|
||||
@@ -155,7 +154,7 @@ impl SimClient {
|
||||
dns_transport: DnsTransport,
|
||||
now: Instant,
|
||||
) -> Option<Transmit> {
|
||||
let Some(sentinel) = self.dns_by_sentinel.get_by_right(&upstream).copied() else {
|
||||
let Some(sentinel) = self.dns_by_sentinel.sentinel_by_upstream(upstream) else {
|
||||
tracing::error!(%upstream, "Unknown DNS server");
|
||||
return None;
|
||||
};
|
||||
@@ -309,8 +308,8 @@ impl SimClient {
|
||||
.expect("ip packets on port 53 to be DNS packets");
|
||||
|
||||
// Map back to upstream socket so we can assert on it correctly.
|
||||
let sentinel = SocketAddr::from((packet.source(), udp.source_port()));
|
||||
let Some(upstream) = self.upstream_dns_by_sentinel(&sentinel) else {
|
||||
let sentinel = packet.source();
|
||||
let Some(upstream) = self.dns_by_sentinel.upstream_by_sentinel(sentinel) else {
|
||||
tracing::error!(%sentinel, mapping = ?self.dns_by_sentinel, "Unknown DNS server");
|
||||
return;
|
||||
};
|
||||
@@ -374,12 +373,6 @@ impl SimClient {
|
||||
)
|
||||
}
|
||||
|
||||
fn upstream_dns_by_sentinel(&self, sentinel: &SocketAddr) -> Option<SocketAddr> {
|
||||
let socket = self.dns_by_sentinel.get_by_left(&sentinel.ip())?;
|
||||
|
||||
Some(*socket)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_dns_response(&mut self, response: &dns_types::Response) {
|
||||
for record in response.records() {
|
||||
#[expect(clippy::wildcard_enum_match_arm)]
|
||||
@@ -1052,7 +1045,9 @@ impl RefClient {
|
||||
///
|
||||
/// If there are upstream DNS servers configured in the portal, it should use those.
|
||||
/// Otherwise it should use whatever was configured on the system prior to connlib starting.
|
||||
pub(crate) fn expected_dns_servers(&self) -> BTreeSet<SocketAddr> {
|
||||
///
|
||||
/// This purposely returns a `Vec` so we also assert the order!
|
||||
pub(crate) fn expected_dns_servers(&self) -> Vec<SocketAddr> {
|
||||
if !self.upstream_dns_resolvers.is_empty() {
|
||||
return self
|
||||
.upstream_dns_resolvers
|
||||
|
||||
@@ -139,7 +139,7 @@ impl SimGateway {
|
||||
|
||||
pub(crate) fn deploy_new_dns_servers(
|
||||
&mut self,
|
||||
dns_servers: impl Iterator<Item = SocketAddr>,
|
||||
dns_servers: impl IntoIterator<Item = SocketAddr>,
|
||||
now: Instant,
|
||||
) {
|
||||
self.udp_dns_server_resources.clear();
|
||||
@@ -151,7 +151,8 @@ impl SimGateway {
|
||||
return;
|
||||
};
|
||||
|
||||
for server in dns_servers
|
||||
for server in iter::empty()
|
||||
.chain(dns_servers)
|
||||
.chain(iter::once(SocketAddr::from((
|
||||
ip_config.v4,
|
||||
tun_dns_server_port,
|
||||
|
||||
@@ -643,10 +643,13 @@ impl TunnelTest {
|
||||
while let Some(result) = c.tcp_dns_client.poll_query_result() {
|
||||
match result.result {
|
||||
Ok(message) => {
|
||||
let upstream = c.dns_mapping().get_by_left(&result.server.ip()).unwrap();
|
||||
let upstream = c
|
||||
.dns_mapping()
|
||||
.upstream_by_sentinel(result.server.ip())
|
||||
.unwrap();
|
||||
|
||||
c.received_tcp_dns_responses
|
||||
.insert((*upstream, result.query.id()));
|
||||
.insert((upstream, result.query.id()));
|
||||
c.handle_dns_response(&message)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -898,10 +901,7 @@ impl TunnelTest {
|
||||
|
||||
for gateway in self.gateways.values_mut() {
|
||||
gateway.exec_mut(|g| {
|
||||
g.deploy_new_dns_servers(
|
||||
config.dns_by_sentinel.right_values().copied(),
|
||||
now,
|
||||
)
|
||||
g.deploy_new_dns_servers(config.dns_by_sentinel.upstream_sockets(), now)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -495,7 +495,7 @@ impl<'a> Handler<'a> {
|
||||
|
||||
self.tun_device.set_ips(config.ip.v4, config.ip.v6).await?;
|
||||
self.dns_controller
|
||||
.set_dns(config.dns_sentinel_ips(), config.search_domain)
|
||||
.set_dns(config.dns_by_sentinel.sentinel_ips(), config.search_domain)
|
||||
.await?;
|
||||
self.tun_device
|
||||
.set_routes(config.ipv4_routes, config.ipv6_routes)
|
||||
|
||||
@@ -409,7 +409,7 @@ fn try_main() -> Result<()> {
|
||||
}
|
||||
client_shared::Event::TunInterfaceUpdated(config) => {
|
||||
tun_device.set_ips(config.ip.v4, config.ip.v6).await?;
|
||||
dns_controller.set_dns(config.dns_sentinel_ips(), config.search_domain).await?;
|
||||
dns_controller.set_dns(config.dns_by_sentinel.sentinel_ips(), config.search_domain).await?;
|
||||
tun_device.set_routes(config.ipv4_routes, config.ipv6_routes).await?;
|
||||
|
||||
// `on_set_interface_config` is guaranteed to be called when the tunnel is completely ready
|
||||
|
||||
@@ -24,6 +24,10 @@ export default function Android() {
|
||||
<ChangeItem pull="10752">
|
||||
Fixes an issue where the reported client version was out of date.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="10773">
|
||||
Fixes an issue where the order of upstream / system DNS resolvers was
|
||||
not respected.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.5.6" date={new Date("2025-10-28")}>
|
||||
<ChangeItem pull="10667">
|
||||
|
||||
@@ -28,6 +28,10 @@ export default function Apple() {
|
||||
<ChangeItem pull="10752">
|
||||
Fixes an issue where the reported client version was out of date.
|
||||
</ChangeItem>
|
||||
<ChangeItem pull="10773">
|
||||
Fixes an issue where the order of upstream / system DNS resolvers was
|
||||
not respected.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.5.9" date={new Date("2025-10-20")}>
|
||||
<ChangeItem pull="10603">
|
||||
|
||||
@@ -17,6 +17,10 @@ export default function GUI({ os }: { os: OS }) {
|
||||
the local network were not routable.
|
||||
</ChangeItem>
|
||||
)}
|
||||
<ChangeItem pull="10773">
|
||||
Fixes an issue where the order of upstream / system DNS resolvers was
|
||||
not respected.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.5.8" date={new Date("2025-10-16")}>
|
||||
<ChangeItem pull="10509">
|
||||
|
||||
@@ -16,6 +16,10 @@ export default function Headless({ os }: { os: OS }) {
|
||||
the local network were not routable.
|
||||
</ChangeItem>
|
||||
)}
|
||||
<ChangeItem pull="10773">
|
||||
Fixes an issue where the order of upstream / system DNS resolvers was
|
||||
not respected.
|
||||
</ChangeItem>
|
||||
</Unreleased>
|
||||
<Entry version="1.5.4" date={new Date("2025-10-16")}>
|
||||
<ChangeItem pull="10533">
|
||||
|
||||
Reference in New Issue
Block a user