diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a9ab889a1..94dcec338 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2720,6 +2720,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tun", + "url", "uuid", ] diff --git a/rust/connlib/tunnel/Cargo.toml b/rust/connlib/tunnel/Cargo.toml index 6f368956e..47678d274 100644 --- a/rust/connlib/tunnel/Cargo.toml +++ b/rust/connlib/tunnel/Cargo.toml @@ -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] diff --git a/rust/connlib/tunnel/proptest-regressions/tests.txt b/rust/connlib/tunnel/proptest-regressions/tests.txt index 8b18e671c..ef1edacc5 100644 --- a/rust/connlib/tunnel/proptest-regressions/tests.txt +++ b/rust/connlib/tunnel/proptest-regressions/tests.txt @@ -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 diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index 3ecf331b4..a6d987a12 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -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"); } diff --git a/rust/connlib/tunnel/src/client/dns_config.rs b/rust/connlib/tunnel/src/client/dns_config.rs index af454b0dd..209a3e153 100644 --- a/rust/connlib/tunnel/src/client/dns_config.rs +++ b/rust/connlib/tunnel/src/client/dns_config.rs @@ -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, + /// The DoH resolvers configured in the portal. + /// + /// Has priority over system-configured DNS servers. + upstream_doh: Vec, /// 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) -> 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) -> 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() } diff --git a/rust/connlib/tunnel/src/messages.rs b/rust/connlib/tunnel/src/messages.rs index 6e7e8047f..bbbece700 100644 --- a/rust/connlib/tunnel/src/messages.rs +++ b/rust/connlib/tunnel/src/messages.rs @@ -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, - + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub upstream_doh: Vec, #[serde(default)] pub search_domain: Option, } @@ -192,6 +195,10 @@ impl Interface { }) .collect() } + + pub fn upstream_doh(&self) -> Vec { + 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")] diff --git a/rust/connlib/tunnel/src/tests/reference.rs b/rust/connlib/tunnel/src/tests/reference.rs index 331c4e819..736f1cfd4 100644 --- a/rust/connlib/tunnel/src/tests/reference.rs +++ b/rust/connlib/tunnel/src/tests/reference.rs @@ -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::(); @@ -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 diff --git a/rust/connlib/tunnel/src/tests/sim_client.rs b/rust/connlib/tunnel/src/tests/sim_client.rs index ee8547613..985c2d608 100644 --- a/rust/connlib/tunnel/src/tests/sim_client.rs +++ b/rust/connlib/tunnel/src/tests/sim_client.rs @@ -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, + /// The upstream DoH resolvers configured in the portal. + #[debug(skip)] + upstream_doh_resolvers: Vec, /// The search-domain configured in the portal. pub(crate) search_domain: Option, @@ -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) { + pub(crate) fn set_upstream_do53_resolvers(&mut self, servers: &Vec) { self.upstream_do53_resolvers.clone_from(servers); } + pub(crate) fn set_upstream_doh_resolvers(&mut self, servers: &Vec) { + 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 { + pub(crate) fn upstream_do53_resolvers(&self) -> Vec { self.upstream_do53_resolvers.clone() } + pub(crate) fn upstream_doh_resolvers(&self) -> Vec { + 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, system_dns: impl Strategy>, upstream_do53: impl Strategy>, + upstream_doh: impl Strategy>, search_domain: impl Strategy>, ) -> impl Strategy> { 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, system_dns: impl Strategy>, upstream_do53: impl Strategy>, + upstream_doh: impl Strategy>, search_domain: impl Strategy>, ) -> impl Strategy { ( @@ -1296,6 +1315,7 @@ fn ref_client( tunnel_ip6s, system_dns, upstream_do53, + upstream_doh, search_domain, any::(), 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(), diff --git a/rust/connlib/tunnel/src/tests/strategies.rs b/rust/connlib/tunnel/src/tests/strategies.rs index cc6da3398..748fd08d2 100644 --- a/rust/connlib/tunnel/src/tests/strategies.rs +++ b/rust/connlib/tunnel/src/tests/strategies.rs @@ -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 { 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> { +pub(crate) fn do53_servers() -> impl Strategy> { let ip4_dns_servers = collection::btree_set(non_reserved_ipv4().prop_map_into::(), 1..4); let ip6_dns_servers = @@ -204,6 +206,15 @@ pub(crate) fn dns_servers() -> impl Strategy> { }) } +pub(crate) fn doh_server() -> impl Strategy { + 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 { prop_oneof![ non_reserved_ipv4().prop_map_into(), @@ -358,7 +369,7 @@ pub(crate) fn documentation_ip6s(subnet: u16) -> impl Strategy } pub(crate) fn system_dns_servers() -> impl Strategy> { - 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> { } pub(crate) fn upstream_do53_servers() -> impl Strategy> { - 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> { + btree_set(doh_server(), 0..2) + .prop_map(|servers| servers.into_iter().map(|url| UpstreamDoH { url }).collect()) +} diff --git a/rust/connlib/tunnel/src/tests/stub_portal.rs b/rust/connlib/tunnel/src/tests/stub_portal.rs index c215a74cb..77b3e5b0f 100644 --- a/rust/connlib/tunnel/src/tests/stub_portal.rs +++ b/rust/connlib/tunnel/src/tests/stub_portal.rs @@ -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( + pub(crate) fn client( &self, system_dns: S1, - upstream_dns: S2, - ) -> impl Strategy> + use + upstream_do53: S2, + upstream_doh: S3, + ) -> impl Strategy> + use where S1: Strategy>, S2: Strategy>, + S3: Strategy>, { ref_client_host( Just(self.client_tunnel_ipv4), Just(self.client_tunnel_ipv6), system_dns, - upstream_dns, + upstream_do53, + upstream_doh, self.search_domain(), ) } diff --git a/rust/connlib/tunnel/src/tests/sut.rs b/rust/connlib/tunnel/src/tests/sut.rs index f3b277316..54eefaf18 100644 --- a/rust/connlib/tunnel/src/tests/sut.rs +++ b/rust/connlib/tunnel/src/tests/sut.rs @@ -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); diff --git a/rust/connlib/tunnel/src/tests/transition.rs b/rust/connlib/tunnel/src/tests/transition.rs index d8afe569a..d4d60a45e 100644 --- a/rust/connlib/tunnel/src/tests/transition.rs +++ b/rust/connlib/tunnel/src/tests/transition.rs @@ -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), /// The upstream Do53 servers changed. UpdateUpstreamDo53Servers(Vec), + /// The upstream DoH servers changed. + UpdateUpstreamDoHServers(Vec), /// The upstream search domain changed. UpdateUpstreamSearchDomain(Option),