chore(connlib): pass through DoH servers to DNS config (#10872)

This is a follow-up to #10851.

In order to be able to use and reason about the DoH servers, we need to
deserialize the list and pass the servers into connlib's `DnsConfig`.
Right now, they just sit there and we don't do anything with them. Thus,
this PR is save to go into `main`, even if we were to make a release
before our DoH support is fully finished.

To ensure this is the case, we also update the proptests in this PR to
randomly sample and apply DoH servers.

---------

Signed-off-by: Thomas Eizinger <thomas@eizinger.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Thomas Eizinger
2025-11-14 09:37:30 +11:00
committed by GitHub
parent b77472095d
commit 33bd31c1eb
12 changed files with 142 additions and 24 deletions

1
rust/Cargo.lock generated
View File

@@ -2720,6 +2720,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"tun",
"url",
"uuid",
]

View File

@@ -54,6 +54,7 @@ tokio = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
tun = { workspace = true }
url = { workspace = true, features = ["serde"] }
uuid = { workspace = true, features = ["std", "v4"] }
[dev-dependencies]

View File

@@ -233,3 +233,12 @@ cc 4bf2050d07594df9fbaf3462fe9dc0463739e42fdd5f614c644c86d37163cdb6
cc 0ca23286f6e2952919d254d7c524e65ece78672d558672f124ea67b37e82f7d5
cc 3a579d80f7bff0b34ad67a2b9156c50eeb5c3f1861793a52539b80b75ca415b1
cc 096d9ba59d5770265ec3dfb185560f28b0dcd2839584eef0918eebe12b63c01c
cc ba2d72a9cca8a1b7d640cd12513ee28c753bbbec0ee1f137d5ab36333e625244
cc 2d8745a993e5b21f93dc882db3a06d83b91db994b5e70c5fb5eb13db78866c64
cc b476827ddf8b599b4d91daebddfea803dde1aaa1caeceab9ef7f72735b076b02
cc 1501b6cbb2686ad6e87f7d9957a5dcd4677ceca6804ff84e820c8ae30deaea74
cc 62be88709a87008a45bb730ea4fa10878757d6572bfdb28942c32f157910a188
cc 2e19d8524474163fb96a33e084832516e2e753a1c3e969f2436ace0850bcd74c
cc 19b20eeea8590ac247e6534e42344cc4ae67a5d8e964f04e81b56f344d257c7b
cc 46d17b15ff020c3f4982c43d85a342c5d10f5cec34a2316282ecfbe3a684573d
cc a2746c27c8acc2f163989297aba492f9c767d7d4b72fef1cb9b84b65e5cbdfea

View File

@@ -1029,13 +1029,16 @@ impl ClientState {
}
pub fn update_interface_config(&mut self, config: InterfaceConfig) {
tracing::trace!(upstream_do53 = ?config.upstream_do53(), search_domain = ?config.search_domain, ipv4 = %config.ipv4, ipv6 = %config.ipv6, "Received interface configuration from portal");
tracing::trace!(upstream_do53 = ?config.upstream_do53(), upstream_doh = ?config.upstream_doh(), search_domain = ?config.search_domain, ipv4 = %config.ipv4, ipv6 = %config.ipv6, "Received interface configuration from portal");
let changed = self
let changed_do53 = self
.dns_config
.update_upstream_do53_resolvers(config.upstream_do53());
let changed_doh = self
.dns_config
.update_upstream_doh_resolvers(config.upstream_doh());
if changed {
if changed_do53 || changed_doh {
self.dns_cache.flush("DNS servers changed");
}

View File

@@ -4,6 +4,7 @@ use std::{
};
use ip_network::IpNetwork;
use url::Url;
use crate::{
client::{DNS_SENTINELS_V4, DNS_SENTINELS_V6, IpProvider},
@@ -18,6 +19,10 @@ pub(crate) struct DnsConfig {
///
/// Has priority over system-configured DNS servers.
upstream_do53: Vec<IpAddr>,
/// The DoH resolvers configured in the portal.
///
/// Has priority over system-configured DNS servers.
upstream_doh: Vec<Url>,
/// Maps from connlib-assigned IP of a DNS server back to the originally configured system DNS resolver.
mapping: DnsMapping,
@@ -74,13 +79,22 @@ impl DnsConfig {
#[must_use = "Check if the DNS mapping has changed"]
pub(crate) fn update_upstream_do53_resolvers(&mut self, servers: Vec<IpAddr>) -> bool {
tracing::debug!(?servers, "Received upstream-defined DNS servers");
tracing::debug!(?servers, "Received upstream-defined Do53 servers");
self.upstream_do53 = servers;
self.update_dns_mapping()
}
#[must_use = "Check if the DNS mapping has changed"]
pub(crate) fn update_upstream_doh_resolvers(&mut self, servers: Vec<Url>) -> bool {
tracing::debug!(?servers, "Received upstream-defined DoH servers");
self.upstream_doh = servers;
self.update_dns_mapping()
}
pub(crate) fn has_custom_upstream(&self) -> bool {
!self.upstream_do53.is_empty()
}

View File

@@ -9,6 +9,7 @@ use ip_network::IpNetwork;
use secrecy::ExposeSecret as _;
use serde::{Deserialize, Serialize};
use std::fmt;
use url::Url;
pub mod client;
pub mod gateway;
@@ -173,7 +174,9 @@ pub struct Interface {
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub upstream_do53: Vec<UpstreamDo53>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub upstream_doh: Vec<UpstreamDoH>,
#[serde(default)]
pub search_domain: Option<DomainName>,
}
@@ -192,6 +195,10 @@ impl Interface {
})
.collect()
}
pub fn upstream_doh(&self) -> Vec<Url> {
self.upstream_doh.iter().map(|u| u.url.clone()).collect()
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
@@ -199,6 +206,11 @@ pub struct UpstreamDo53 {
pub ip: IpAddr,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct UpstreamDoH {
pub url: Url,
}
/// A single relay
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]

View File

@@ -60,7 +60,11 @@ impl ReferenceState {
.prop_flat_map(move |portal| {
let gateways = portal.gateways(start);
let dns_resource_records = portal.dns_resource_records(start);
let client = portal.client(system_dns_servers(), upstream_do53_servers());
let client = portal.client(
system_dns_servers(),
upstream_do53_servers(),
upstream_doh_servers(),
);
let relays = relays(relay_id());
let global_dns_records = global_dns_records(start); // Start out with a set of global DNS records so we have something to resolve outside of DNS resources.
let drop_direct_client_traffic = any::<bool>();
@@ -221,6 +225,10 @@ impl ReferenceState {
1,
upstream_do53_servers().prop_map(Transition::UpdateUpstreamDo53Servers),
)
.with(
1,
upstream_doh_servers().prop_map(Transition::UpdateUpstreamDoHServers),
)
.with(
1,
state
@@ -577,7 +585,12 @@ impl ReferenceState {
Transition::UpdateUpstreamDo53Servers(servers) => {
state
.client
.exec_mut(|client| client.set_upstream_dns_resolvers(servers));
.exec_mut(|client| client.set_upstream_do53_resolvers(servers));
}
Transition::UpdateUpstreamDoHServers(servers) => {
state
.client
.exec_mut(|client| client.set_upstream_doh_resolvers(servers));
}
Transition::UpdateUpstreamSearchDomain(domain) => {
state
@@ -740,6 +753,7 @@ impl ReferenceState {
.iter()
.any(|dns_server| state.client.sending_socket_for(dns_server.ip).is_some())
}
Transition::UpdateUpstreamDoHServers(_) => true,
Transition::UpdateUpstreamSearchDomain(_) => true,
Transition::SendDnsQueries(queries) => queries.iter().all(|query| {
let has_socket_for_server = state

View File

@@ -7,7 +7,11 @@ use super::{
strategies::latency,
transition::{DPort, Destination, DnsQuery, DnsTransport, Identifier, SPort, Seq},
};
use crate::{ClientState, DnsMapping, DnsResourceRecord, messages::UpstreamDo53, proptest::*};
use crate::{
ClientState, DnsMapping, DnsResourceRecord,
messages::{UpstreamDo53, UpstreamDoH},
proptest::*,
};
use crate::{
client::{CidrResource, DnsResource, InternetResource, Resource},
messages::Interface,
@@ -423,6 +427,9 @@ pub struct RefClient {
/// The upstream Do53 resolvers configured in the portal.
#[debug(skip)]
upstream_do53_resolvers: Vec<UpstreamDo53>,
/// The upstream DoH resolvers configured in the portal.
#[debug(skip)]
upstream_doh_resolvers: Vec<UpstreamDoH>,
/// The search-domain configured in the portal.
pub(crate) search_domain: Option<DomainName>,
@@ -502,6 +509,7 @@ impl RefClient {
ipv6: self.tunnel_ip6,
upstream_dns: Vec::new(),
upstream_do53: self.upstream_do53_resolvers.clone(),
upstream_doh: self.upstream_doh_resolvers,
search_domain: self.search_domain.clone(),
});
client_state.update_system_resolvers(self.system_dns_resolvers.clone());
@@ -1209,18 +1217,26 @@ impl RefClient {
self.system_dns_resolvers.clone_from(servers);
}
pub(crate) fn set_upstream_dns_resolvers(&mut self, servers: &Vec<UpstreamDo53>) {
pub(crate) fn set_upstream_do53_resolvers(&mut self, servers: &Vec<UpstreamDo53>) {
self.upstream_do53_resolvers.clone_from(servers);
}
pub(crate) fn set_upstream_doh_resolvers(&mut self, servers: &Vec<UpstreamDoH>) {
self.upstream_doh_resolvers.clone_from(servers);
}
pub(crate) fn set_upstream_search_domain(&mut self, domain: Option<&DomainName>) {
self.search_domain = domain.cloned()
}
pub(crate) fn upstream_dns_resolvers(&self) -> Vec<UpstreamDo53> {
pub(crate) fn upstream_do53_resolvers(&self) -> Vec<UpstreamDo53> {
self.upstream_do53_resolvers.clone()
}
pub(crate) fn upstream_doh_resolvers(&self) -> Vec<UpstreamDoH> {
self.upstream_doh_resolvers.clone()
}
pub(crate) fn has_tcp_connection(
&self,
src: IpAddr,
@@ -1268,6 +1284,7 @@ pub(crate) fn ref_client_host(
tunnel_ip6s: impl Strategy<Value = Ipv6Addr>,
system_dns: impl Strategy<Value = Vec<IpAddr>>,
upstream_do53: impl Strategy<Value = Vec<UpstreamDo53>>,
upstream_doh: impl Strategy<Value = Vec<UpstreamDoH>>,
search_domain: impl Strategy<Value = Option<DomainName>>,
) -> impl Strategy<Value = Host<RefClient>> {
host(
@@ -1278,6 +1295,7 @@ pub(crate) fn ref_client_host(
tunnel_ip6s,
system_dns,
upstream_do53,
upstream_doh,
search_domain,
),
latency(250), // TODO: Increase with #6062.
@@ -1289,6 +1307,7 @@ fn ref_client(
tunnel_ip6s: impl Strategy<Value = Ipv6Addr>,
system_dns: impl Strategy<Value = Vec<IpAddr>>,
upstream_do53: impl Strategy<Value = Vec<UpstreamDo53>>,
upstream_doh: impl Strategy<Value = Vec<UpstreamDoH>>,
search_domain: impl Strategy<Value = Option<DomainName>>,
) -> impl Strategy<Value = RefClient> {
(
@@ -1296,6 +1315,7 @@ fn ref_client(
tunnel_ip6s,
system_dns,
upstream_do53,
upstream_doh,
search_domain,
any::<bool>(),
client_id(),
@@ -1307,6 +1327,7 @@ fn ref_client(
tunnel_ip6,
system_dns_resolvers,
upstream_do53_resolvers,
upstream_doh_resolvers,
search_domain,
internet_resource_active,
id,
@@ -1319,6 +1340,7 @@ fn ref_client(
tunnel_ip6,
system_dns_resolvers,
upstream_do53_resolvers,
upstream_doh_resolvers,
search_domain,
internet_resource_active,
cidr_resources: IpNetworkTable::new(),

View File

@@ -5,13 +5,14 @@ use crate::client::{
CidrResource, DNS_SENTINELS_V4, DNS_SENTINELS_V6, DnsResource, IPV4_RESOURCES, IPV6_RESOURCES,
InternetResource,
};
use crate::messages::UpstreamDo53;
use crate::messages::{UpstreamDo53, UpstreamDoH};
use crate::{IPV4_TUNNEL, IPV6_TUNNEL, proptest::*};
use connlib_model::{RelayId, Site};
use dns_types::{DomainName, OwnedRecordData};
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
use itertools::Itertools;
use prop::sample;
use proptest::collection::btree_set;
use proptest::{collection, prelude::*};
use std::iter;
use std::num::NonZeroU16;
@@ -21,6 +22,7 @@ use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
time::Duration,
};
use url::Url;
pub(crate) fn global_dns_records(at: Instant) -> impl Strategy<Value = DnsRecords> {
collection::btree_map(
@@ -189,10 +191,10 @@ pub(crate) fn relays(
collection::btree_map(id, ref_relay_host(), 1..=2)
}
/// Sample a list of DNS servers.
/// Sample a list of Do53 servers.
///
/// We make sure to always have at least 1 IPv4 and 1 IPv6 DNS server.
pub(crate) fn dns_servers() -> impl Strategy<Value = BTreeSet<IpAddr>> {
pub(crate) fn do53_servers() -> impl Strategy<Value = BTreeSet<IpAddr>> {
let ip4_dns_servers =
collection::btree_set(non_reserved_ipv4().prop_map_into::<IpAddr>(), 1..4);
let ip6_dns_servers =
@@ -204,6 +206,15 @@ pub(crate) fn dns_servers() -> impl Strategy<Value = BTreeSet<IpAddr>> {
})
}
pub(crate) fn doh_server() -> impl Strategy<Value = Url> {
prop_oneof![
Just(Url::parse("https://dns.quad9.net/dns-query").unwrap()),
Just(Url::parse("https://cloudflare-dns.com/dns-query").unwrap()),
Just(Url::parse("https://dns.google/dns-query").unwrap()),
Just(Url::parse("https://dns.opendoh.com/dns-query").unwrap()),
]
}
pub(crate) fn non_reserved_ip() -> impl Strategy<Value = IpAddr> {
prop_oneof![
non_reserved_ipv4().prop_map_into(),
@@ -358,7 +369,7 @@ pub(crate) fn documentation_ip6s(subnet: u16) -> impl Strategy<Value = Ipv6Addr>
}
pub(crate) fn system_dns_servers() -> impl Strategy<Value = Vec<IpAddr>> {
dns_servers().prop_flat_map(|dns_servers| {
do53_servers().prop_flat_map(|dns_servers| {
let max = dns_servers.len();
sample::subsequence(Vec::from_iter(dns_servers), ..=max)
@@ -366,10 +377,15 @@ pub(crate) fn system_dns_servers() -> impl Strategy<Value = Vec<IpAddr>> {
}
pub(crate) fn upstream_do53_servers() -> impl Strategy<Value = Vec<UpstreamDo53>> {
dns_servers().prop_flat_map(|dns_servers| {
do53_servers().prop_flat_map(|dns_servers| {
let max = dns_servers.len();
sample::subsequence(Vec::from_iter(dns_servers), ..=max)
.prop_map(|seq| seq.into_iter().map(|ip| UpstreamDo53 { ip }).collect())
})
}
pub(crate) fn upstream_doh_servers() -> impl Strategy<Value = Vec<UpstreamDoH>> {
btree_set(doh_server(), 0..2)
.prop_map(|servers| servers.into_iter().map(|url| UpstreamDoH { url }).collect())
}

View File

@@ -5,7 +5,11 @@ use super::{
sim_net::Host,
strategies::{resolved_ips, site_specific_dns_record, subdomain_records},
};
use crate::{client, messages::UpstreamDo53, proptest::*};
use crate::{
client,
messages::{UpstreamDo53, UpstreamDoH},
proptest::*,
};
use crate::{client::DnsResource, messages::gateway};
use connlib_model::{GatewayId, Site};
use connlib_model::{ResourceId, SiteId};
@@ -281,20 +285,23 @@ impl StubPortal {
.prop_map(BTreeMap::from_iter)
}
pub(crate) fn client<S1, S2>(
pub(crate) fn client<S1, S2, S3>(
&self,
system_dns: S1,
upstream_dns: S2,
) -> impl Strategy<Value = Host<RefClient>> + use<S1, S2>
upstream_do53: S2,
upstream_doh: S3,
) -> impl Strategy<Value = Host<RefClient>> + use<S1, S2, S3>
where
S1: Strategy<Value = Vec<IpAddr>>,
S2: Strategy<Value = Vec<UpstreamDo53>>,
S3: Strategy<Value = Vec<UpstreamDoH>>,
{
ref_client_host(
Just(self.client_tunnel_ipv4),
Just(self.client_tunnel_ipv6),
system_dns,
upstream_dns,
upstream_do53,
upstream_doh,
self.search_domain(),
)
}

View File

@@ -277,6 +277,19 @@ impl TunnelTest {
upstream_dns: vec![],
upstream_do53,
search_domain: ref_state.client.inner().search_domain.clone(),
upstream_doh: vec![],
})
});
}
Transition::UpdateUpstreamDoHServers(upstream_doh) => {
state.client.exec_mut(|c| {
c.sut.update_interface_config(Interface {
ipv4: c.sut.tunnel_ip_config().unwrap().v4,
ipv6: c.sut.tunnel_ip_config().unwrap().v6,
upstream_dns: vec![],
upstream_do53: ref_state.client.inner().upstream_do53_resolvers(),
search_domain: ref_state.client.inner().search_domain.clone(),
upstream_doh,
})
});
}
@@ -286,7 +299,8 @@ impl TunnelTest {
ipv4: c.sut.tunnel_ip_config().unwrap().v4,
ipv6: c.sut.tunnel_ip_config().unwrap().v6,
upstream_dns: vec![],
upstream_do53: ref_state.client.inner().upstream_dns_resolvers(),
upstream_do53: ref_state.client.inner().upstream_do53_resolvers(),
upstream_doh: ref_state.client.inner().upstream_doh_resolvers(),
search_domain,
})
});
@@ -312,7 +326,8 @@ impl TunnelTest {
Transition::ReconnectPortal => {
let ipv4 = state.client.inner().sut.tunnel_ip_config().unwrap().v4;
let ipv6 = state.client.inner().sut.tunnel_ip_config().unwrap().v6;
let upstream_do53 = ref_state.client.inner().upstream_dns_resolvers();
let upstream_do53 = ref_state.client.inner().upstream_do53_resolvers();
let upstream_doh = ref_state.client.inner().upstream_doh_resolvers();
let all_resources = ref_state.client.inner().all_resources();
// Simulate receiving `init`.
@@ -322,6 +337,7 @@ impl TunnelTest {
ipv6,
upstream_dns: Vec::new(),
upstream_do53,
upstream_doh,
search_domain: ref_state.client.inner().search_domain.clone(),
});
c.update_relays(iter::empty(), state.relays.iter(), now);
@@ -405,7 +421,7 @@ impl TunnelTest {
let ipv4 = state.client.inner().sut.tunnel_ip_config().unwrap().v4;
let ipv6 = state.client.inner().sut.tunnel_ip_config().unwrap().v6;
let system_dns = ref_state.client.inner().system_dns_resolvers();
let upstream_do53 = ref_state.client.inner().upstream_dns_resolvers();
let upstream_do53 = ref_state.client.inner().upstream_do53_resolvers();
let all_resources = ref_state.client.inner().all_resources();
let internet_resource_state = ref_state.client.inner().internet_resource_active;
@@ -419,6 +435,7 @@ impl TunnelTest {
upstream_dns: Vec::new(),
upstream_do53,
search_domain: ref_state.client.inner().search_domain.clone(),
upstream_doh: Vec::new(),
});
c.sut.update_system_resolvers(system_dns);
c.sut.set_resources(all_resources, now);

View File

@@ -1,6 +1,6 @@
use crate::{
client::{CidrResource, IPV4_RESOURCES, IPV6_RESOURCES, Resource},
messages::UpstreamDo53,
messages::{UpstreamDo53, UpstreamDoH},
proptest::{host_v4, host_v6},
};
use connlib_model::{RelayId, ResourceId, Site};
@@ -68,6 +68,8 @@ pub(crate) enum Transition {
UpdateSystemDnsServers(Vec<IpAddr>),
/// The upstream Do53 servers changed.
UpdateUpstreamDo53Servers(Vec<UpstreamDo53>),
/// The upstream DoH servers changed.
UpdateUpstreamDoHServers(Vec<UpstreamDoH>),
/// The upstream search domain changed.
UpdateUpstreamSearchDomain(Option<DomainName>),