mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(connlib): allow controlling IP stack per DNS resource (#9300)
With this patch, `connlib` exposes a new, optional field `ip_stack` within the resource description of each DNS resource that controls the supported IP stack. By default, the IP stack is set to `Dual` to preserve the current behaviour. When set to `IPv4Only` or `IPv6Only`, `connlib` will not assign any IPv4 or IPv6 addresses when receiving DNS queries for such a resource. The DNS query will still respond successfully with NOERROR (and not NXDOMAIN) but the list of IPs will be empty. This is useful to e.g. allow sys-admins to disable IPv6 for resources with buggy clients such as the MongoDB atlas driver. The MongoDB driver does not correctly handle happy-eyeballs and instead fails the connection early on any connection error. Additionally, customers operating in IPv6-exclusive networks can disable IPv4 addresses with this setting. Related: https://jira.mongodb.org/browse/NODE-4678 Related: #9042 Related: #8892
This commit is contained in:
@@ -184,3 +184,28 @@ impl fmt::Debug for SiteId {
|
||||
fmt::Display::fmt(&self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// The IP stack of a DNS resource.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum IpStack {
|
||||
Dual,
|
||||
Ipv4Only,
|
||||
Ipv6Only,
|
||||
}
|
||||
|
||||
impl IpStack {
|
||||
pub fn supports_ipv4(&self) -> bool {
|
||||
match self {
|
||||
IpStack::Ipv4Only | IpStack::Dual => true,
|
||||
IpStack::Ipv6Only => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supports_ipv6(&self) -> bool {
|
||||
match self {
|
||||
IpStack::Ipv4Only => false,
|
||||
IpStack::Ipv6Only | IpStack::Dual => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,3 +166,4 @@ cc f2de44e6762e9a681d624467fd19ac9fc00f000dfc1c2a3bda05c905b01674c2
|
||||
cc 36a7bb4eff285399b9c431675d4337712e7edf016a3a02b05cba5115c8bf8fe4
|
||||
cc 235333b8c818e464ba339e8c73b2467894d68d594ac896c4f6a36b25ac6b823d
|
||||
cc 436afa9076f65f9abbe801ef2a7f26631e433650a6f717358972f37a1fbf1542
|
||||
cc ee518414c1632fb9d49272b985476de0d9de2786cadef997ad7d626e1a4b975a
|
||||
|
||||
@@ -1567,7 +1567,10 @@ impl ClientState {
|
||||
}
|
||||
|
||||
let activated = match &new_resource {
|
||||
Resource::Dns(dns) => self.stub_resolver.add_resource(dns.id, dns.address.clone()),
|
||||
Resource::Dns(dns) => {
|
||||
self.stub_resolver
|
||||
.add_resource(dns.id, dns.address.clone(), dns.ip_stack)
|
||||
}
|
||||
Resource::Cidr(cidr) => {
|
||||
let existing = self.active_cidr_resources.exact_match(cidr.address);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use connlib_model::{
|
||||
CidrResourceView, DnsResourceView, InternetResourceView, ResourceId, ResourceStatus,
|
||||
CidrResourceView, DnsResourceView, InternetResourceView, IpStack, ResourceId, ResourceStatus,
|
||||
ResourceView, Site,
|
||||
};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
@@ -35,6 +35,8 @@ pub struct DnsResource {
|
||||
|
||||
pub address_description: Option<String>,
|
||||
pub sites: Vec<Site>,
|
||||
|
||||
pub ip_stack: IpStack,
|
||||
}
|
||||
|
||||
/// Description of a resource that maps to a CIDR.
|
||||
@@ -249,6 +251,7 @@ impl DnsResource {
|
||||
name: resource.name,
|
||||
address_description: resource.address_description,
|
||||
sites: resource.sites,
|
||||
ip_stack: resource.ip_stack.unwrap_or(IpStack::Dual),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,3 +266,83 @@ impl DnsResource {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_dns_resource_with_ipv4_only_ip_stack() {
|
||||
let resource = Resource::from_description(ResourceDescription::Dns(serde_json::json!({
|
||||
"address": "example.com",
|
||||
"id": "03000143-e25e-45c7-aafb-144990e57dce",
|
||||
"name": "example.com",
|
||||
"gateway_groups": [{"name": "test", "id": "bf56f32d-7b2c-4f5d-a784-788977d014a4"}],
|
||||
"type": "dns",
|
||||
"ip_stack": "ipv4_only"
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
let Resource::Dns(dns) = resource else {
|
||||
panic!("Unexpected resource")
|
||||
};
|
||||
|
||||
assert_eq!(dns.ip_stack, IpStack::Ipv4Only)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_dns_resource_with_ipv6_only_ip_stack() {
|
||||
let resource = Resource::from_description(ResourceDescription::Dns(serde_json::json!({
|
||||
"address": "example.com",
|
||||
"id": "03000143-e25e-45c7-aafb-144990e57dce",
|
||||
"name": "example.com",
|
||||
"gateway_groups": [{"name": "test", "id": "bf56f32d-7b2c-4f5d-a784-788977d014a4"}],
|
||||
"type": "dns",
|
||||
"ip_stack": "ipv6_only"
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
let Resource::Dns(dns) = resource else {
|
||||
panic!("Unexpected resource")
|
||||
};
|
||||
|
||||
assert_eq!(dns.ip_stack, IpStack::Ipv6Only)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_dns_resource_with_dual_ip_stack() {
|
||||
let resource = Resource::from_description(ResourceDescription::Dns(serde_json::json!({
|
||||
"address": "example.com",
|
||||
"id": "03000143-e25e-45c7-aafb-144990e57dce",
|
||||
"name": "example.com",
|
||||
"gateway_groups": [{"name": "test", "id": "bf56f32d-7b2c-4f5d-a784-788977d014a4"}],
|
||||
"type": "dns",
|
||||
"ip_stack": "dual"
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
let Resource::Dns(dns) = resource else {
|
||||
panic!("Unexpected resource")
|
||||
};
|
||||
|
||||
assert_eq!(dns.ip_stack, IpStack::Dual)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_deserialize_dns_resource_with_no_stack() {
|
||||
let resource = Resource::from_description(ResourceDescription::Dns(serde_json::json!({
|
||||
"address": "example.com",
|
||||
"id": "03000143-e25e-45c7-aafb-144990e57dce",
|
||||
"name": "example.com",
|
||||
"gateway_groups": [{"name": "test", "id": "bf56f32d-7b2c-4f5d-a784-788977d014a4"}],
|
||||
"type": "dns"
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
let Resource::Dns(dns) = resource else {
|
||||
panic!("Unexpected resource")
|
||||
};
|
||||
|
||||
assert_eq!(dns.ip_stack, IpStack::Dual)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::client::IpProvider;
|
||||
use anyhow::Result;
|
||||
use connlib_model::ResourceId;
|
||||
use connlib_model::{IpStack, ResourceId};
|
||||
use dns_types::{
|
||||
DomainName, DomainNameRef, OwnedRecordData, Query, RecordType, Response, ResponseBuilder,
|
||||
ResponseCode,
|
||||
@@ -38,10 +38,16 @@ pub struct StubResolver {
|
||||
ips_to_fqdn: HashMap<IpAddr, (dns_types::DomainName, ResourceId)>,
|
||||
ip_provider: IpProvider,
|
||||
/// All DNS resources we know about, indexed by the glob pattern they match against.
|
||||
dns_resources: BTreeMap<Pattern, ResourceId>,
|
||||
dns_resources: BTreeMap<Pattern, Resource>,
|
||||
search_domain: Option<DomainName>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Resource {
|
||||
id: ResourceId,
|
||||
ip_stack: IpStack,
|
||||
}
|
||||
|
||||
/// A query that needs to be forwarded to an upstream DNS server for resolution.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RecursiveQuery {
|
||||
@@ -141,7 +147,12 @@ impl StubResolver {
|
||||
.map(|((domain, resource), ips)| (domain, resource, ips))
|
||||
}
|
||||
|
||||
pub(crate) fn add_resource(&mut self, id: ResourceId, pattern: String) -> bool {
|
||||
pub(crate) fn add_resource(
|
||||
&mut self,
|
||||
id: ResourceId,
|
||||
pattern: String,
|
||||
ip_stack: IpStack,
|
||||
) -> bool {
|
||||
let parsed_pattern = match Pattern::new(&pattern) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
@@ -150,21 +161,23 @@ impl StubResolver {
|
||||
}
|
||||
};
|
||||
|
||||
let existing = self.dns_resources.insert(parsed_pattern, id);
|
||||
let existing = self
|
||||
.dns_resources
|
||||
.insert(parsed_pattern, Resource { id, ip_stack });
|
||||
|
||||
existing.is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn remove_resource(&mut self, id: ResourceId) {
|
||||
self.dns_resources.retain(|_, r| *r != id);
|
||||
self.dns_resources.retain(|_, r| r.id != id);
|
||||
}
|
||||
|
||||
fn get_or_assign_a_records(
|
||||
&mut self,
|
||||
fqdn: dns_types::DomainName,
|
||||
resource_id: ResourceId,
|
||||
resource: Resource,
|
||||
) -> Vec<OwnedRecordData> {
|
||||
self.get_or_assign_ips(fqdn, resource_id)
|
||||
self.get_or_assign_ips(fqdn, resource)
|
||||
.into_iter()
|
||||
.filter_map(get_v4)
|
||||
.map(dns_types::records::a)
|
||||
@@ -174,9 +187,9 @@ impl StubResolver {
|
||||
fn get_or_assign_aaaa_records(
|
||||
&mut self,
|
||||
fqdn: dns_types::DomainName,
|
||||
resource_id: ResourceId,
|
||||
resource: Resource,
|
||||
) -> Vec<OwnedRecordData> {
|
||||
self.get_or_assign_ips(fqdn, resource_id)
|
||||
self.get_or_assign_ips(fqdn, resource)
|
||||
.into_iter()
|
||||
.filter_map(get_v6)
|
||||
.map(dns_types::records::aaaa)
|
||||
@@ -186,14 +199,21 @@ impl StubResolver {
|
||||
fn get_or_assign_ips(
|
||||
&mut self,
|
||||
fqdn: dns_types::DomainName,
|
||||
resource_id: ResourceId,
|
||||
resource: Resource,
|
||||
) -> Vec<IpAddr> {
|
||||
let ips = self
|
||||
.fqdn_to_ips
|
||||
.entry((fqdn.clone(), resource_id))
|
||||
.entry((fqdn.clone(), resource.id))
|
||||
.or_insert_with(|| {
|
||||
let mut ips = self.ip_provider.get_n_ipv4(4);
|
||||
ips.extend_from_slice(&self.ip_provider.get_n_ipv6(4));
|
||||
let mut ips = Vec::with_capacity(8);
|
||||
|
||||
if resource.ip_stack.supports_ipv4() {
|
||||
ips.extend(self.ip_provider.get_n_ipv4(4));
|
||||
}
|
||||
|
||||
if resource.ip_stack.supports_ipv6() {
|
||||
ips.extend(self.ip_provider.get_n_ipv6(4));
|
||||
}
|
||||
|
||||
tracing::debug!(domain = %fqdn, ?ips, "Assigning proxy IPs");
|
||||
|
||||
@@ -201,7 +221,7 @@ impl StubResolver {
|
||||
})
|
||||
.clone();
|
||||
for ip in &ips {
|
||||
self.ips_to_fqdn.insert(*ip, (fqdn.clone(), resource_id));
|
||||
self.ips_to_fqdn.insert(*ip, (fqdn.clone(), resource.id));
|
||||
}
|
||||
|
||||
ips
|
||||
@@ -210,16 +230,16 @@ impl StubResolver {
|
||||
/// Attempts to match the given domain against our list of possible patterns.
|
||||
///
|
||||
/// This performs a linear search and is thus O(N) and **must not** be called in the hot-path of packet routing.
|
||||
fn match_resource_linear(&self, domain: &dns_types::DomainName) -> Option<ResourceId> {
|
||||
fn match_resource_linear(&self, domain: &dns_types::DomainName) -> Option<Resource> {
|
||||
let _span = telemetry_span!("match_resource_linear").entered();
|
||||
|
||||
let name = Candidate::from_domain(domain);
|
||||
|
||||
for (pattern, id) in &self.dns_resources {
|
||||
for (pattern, r) in &self.dns_resources {
|
||||
if pattern.matches(&name) {
|
||||
tracing::trace!(%id, %pattern, %domain, "Matched resource");
|
||||
tracing::trace!(id = %r.id, %pattern, %domain, "Matched resource");
|
||||
|
||||
return Some(*id);
|
||||
return Some(*r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,9 +285,9 @@ impl StubResolver {
|
||||
self.get_or_assign_aaaa_records(domain.clone(), resource)
|
||||
}
|
||||
(RecordType::SRV | RecordType::TXT, Some(resource)) => {
|
||||
tracing::debug!(%qtype, %resource, "Forwarding query for DNS resource to corresponding site");
|
||||
tracing::debug!(%qtype, resource = %resource.id, "Forwarding query for DNS resource to corresponding site");
|
||||
|
||||
return ResolveStrategy::RecurseSite(resource);
|
||||
return ResolveStrategy::RecurseSite(resource.id);
|
||||
}
|
||||
(RecordType::PTR, _) => {
|
||||
let Some(fqdn) = self.resource_address_name_by_reservse_dns(&domain) else {
|
||||
@@ -641,14 +661,14 @@ mod tests {
|
||||
let wc = ResourceId::from_u128(0);
|
||||
let non_wc = ResourceId::from_u128(1);
|
||||
|
||||
resolver.add_resource(wc, "**.example.com".to_owned());
|
||||
resolver.add_resource(non_wc, "foo.example.com".to_owned());
|
||||
resolver.add_resource(wc, "**.example.com".to_owned(), IpStack::Dual);
|
||||
resolver.add_resource(non_wc, "foo.example.com".to_owned(), IpStack::Dual);
|
||||
|
||||
let resource_id = resolver
|
||||
let resource = resolver
|
||||
.match_resource_linear(&"foo.example.com".parse().unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resource_id, non_wc);
|
||||
assert_eq!(resource.id, non_wc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -674,6 +694,52 @@ mod tests {
|
||||
assert_eq!(response.response_code(), ResponseCode::NXDOMAIN);
|
||||
assert_eq!(response.records().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_query_for_ipv6_only_resource_yields_empty_set() {
|
||||
let mut resolver = StubResolver::default();
|
||||
|
||||
resolver.add_resource(
|
||||
ResourceId::from_u128(1),
|
||||
"example.com".to_owned(),
|
||||
IpStack::Ipv6Only,
|
||||
);
|
||||
|
||||
let query = Query::new(
|
||||
"example.com".parse::<dns_types::DomainName>().unwrap(),
|
||||
RecordType::A,
|
||||
);
|
||||
|
||||
let ResolveStrategy::LocalResponse(response) = resolver.handle(&query) else {
|
||||
panic!("Unexpected result")
|
||||
};
|
||||
|
||||
assert_eq!(response.response_code(), ResponseCode::NOERROR);
|
||||
assert_eq!(response.records().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aaaa_query_for_ipv4_only_resource_yields_empty_set() {
|
||||
let mut resolver = StubResolver::default();
|
||||
|
||||
resolver.add_resource(
|
||||
ResourceId::from_u128(1),
|
||||
"example.com".to_owned(),
|
||||
IpStack::Ipv4Only,
|
||||
);
|
||||
|
||||
let query = Query::new(
|
||||
"example.com".parse::<dns_types::DomainName>().unwrap(),
|
||||
RecordType::AAAA,
|
||||
);
|
||||
|
||||
let ResolveStrategy::LocalResponse(response) = resolver.handle(&query) else {
|
||||
panic!("Unexpected result")
|
||||
};
|
||||
|
||||
assert_eq!(response.response_code(), ResponseCode::NOERROR);
|
||||
assert_eq!(response.records().count(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "divan")]
|
||||
@@ -692,7 +758,11 @@ mod benches {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for n in 0..NUM_RES {
|
||||
resolver.add_resource(ResourceId::from_u128(n), make_domain(&mut rng));
|
||||
resolver.add_resource(
|
||||
ResourceId::from_u128(n),
|
||||
make_domain(&mut rng),
|
||||
IpStack::Dual,
|
||||
);
|
||||
}
|
||||
|
||||
let needle = resolver
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Client related messages that are needed within connlib
|
||||
|
||||
use crate::messages::{IceCredentials, Interface, Key, Relay, RelaysPresence, SecretKey};
|
||||
use connlib_model::{GatewayId, ResourceId, Site, SiteId};
|
||||
use connlib_model::{GatewayId, IpStack, ResourceId, Site, SiteId};
|
||||
use ip_network::IpNetwork;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -24,6 +24,10 @@ pub struct ResourceDescriptionDns {
|
||||
pub address_description: Option<String>,
|
||||
#[serde(rename = "gateway_groups")]
|
||||
pub sites: Vec<Site>,
|
||||
|
||||
/// The IP stack supported by this resource.
|
||||
#[serde(default)]
|
||||
pub ip_stack: Option<IpStack>,
|
||||
}
|
||||
|
||||
/// Description of a resource that maps to a CIDR.
|
||||
|
||||
@@ -29,7 +29,10 @@ pub mod dns_resource_nat {
|
||||
domain: DomainName,
|
||||
proxy_ips: Vec<IpAddr>,
|
||||
) -> Result<IpPacket> {
|
||||
anyhow::ensure!(proxy_ips.len() == 8, "Expected 8 proxy IPs");
|
||||
anyhow::ensure!(
|
||||
proxy_ips.len() == 4 || proxy_ips.len() == 8,
|
||||
"Expected 4 or 8 proxy IPs"
|
||||
);
|
||||
|
||||
let payload = serde_json::to_vec(&AssignedIps {
|
||||
resource,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use connlib_model::{ClientId, GatewayId, RelayId, ResourceId, Site, SiteId};
|
||||
use connlib_model::{ClientId, GatewayId, IpStack, RelayId, ResourceId, Site, SiteId};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use proptest::{
|
||||
arbitrary::{any, any_with},
|
||||
@@ -32,15 +32,17 @@ pub fn dns_resource(sites: impl Strategy<Value = Vec<Site>>) -> impl Strategy<Va
|
||||
resource_name(),
|
||||
domain_name(2..4),
|
||||
address_description(),
|
||||
ip_stack(),
|
||||
sites,
|
||||
)
|
||||
.prop_map(
|
||||
move |(id, name, address, address_description, sites)| DnsResource {
|
||||
move |(id, name, address, address_description, ip_stack, sites)| DnsResource {
|
||||
id,
|
||||
address,
|
||||
name,
|
||||
sites,
|
||||
address_description,
|
||||
ip_stack,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -90,6 +92,14 @@ pub fn site() -> impl Strategy<Value = Site> + Clone {
|
||||
(site_name(), site_id()).prop_map(|(name, id)| Site { name, id })
|
||||
}
|
||||
|
||||
pub fn ip_stack() -> impl Strategy<Value = IpStack> + Clone {
|
||||
prop_oneof![
|
||||
Just(IpStack::Dual),
|
||||
Just(IpStack::Ipv4Only),
|
||||
Just(IpStack::Ipv6Only)
|
||||
]
|
||||
}
|
||||
|
||||
pub fn resource_id() -> impl Strategy<Value = ResourceId> + Clone {
|
||||
any::<u128>().prop_map(ResourceId::from_u128).no_shrink()
|
||||
}
|
||||
|
||||
@@ -696,7 +696,7 @@ impl ReferenceState {
|
||||
let Some(resource) = self.client.inner().dns_resource_by_domain(name) else {
|
||||
return false;
|
||||
};
|
||||
let Some(gateway) = self.portal.gateway_for_resource(resource) else {
|
||||
let Some(gateway) = self.portal.gateway_for_resource(resource.id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -825,8 +825,8 @@ impl RefClient {
|
||||
fn resource_by_dst(&self, destination: &Destination) -> Option<ResourceId> {
|
||||
match destination {
|
||||
Destination::DomainName { name, .. } => {
|
||||
if let Some(id) = self.dns_resource_by_domain(name) {
|
||||
return Some(id);
|
||||
if let Some(r) = self.dns_resource_by_domain(name) {
|
||||
return Some(r.id);
|
||||
}
|
||||
}
|
||||
Destination::IpAddr(addr) => {
|
||||
@@ -839,7 +839,7 @@ impl RefClient {
|
||||
self.active_internet_resource()
|
||||
}
|
||||
|
||||
pub(crate) fn dns_resource_by_domain(&self, domain: &DomainName) -> Option<ResourceId> {
|
||||
pub(crate) fn dns_resource_by_domain(&self, domain: &DomainName) -> Option<DnsResource> {
|
||||
self.resources
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -847,8 +847,7 @@ impl RefClient {
|
||||
.filter(|r| is_subdomain(&domain.to_string(), &r.address))
|
||||
.sorted_by_key(|r| r.address.len())
|
||||
.rev()
|
||||
.map(|r| r.id)
|
||||
.find(|id| !self.disabled_resources.contains(id))
|
||||
.find(|r| !self.disabled_resources.contains(&r.id))
|
||||
}
|
||||
|
||||
fn resolved_domains(&self) -> impl Iterator<Item = (DomainName, BTreeSet<RecordType>)> + '_ {
|
||||
@@ -900,6 +899,10 @@ impl RefClient {
|
||||
.any(|r| matches!(r, &RecordType::A))
|
||||
.then_some(domain)
|
||||
})
|
||||
.filter(|d| {
|
||||
self.dns_resource_by_domain(d)
|
||||
.is_some_and(|r| r.ip_stack.supports_ipv4())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -911,6 +914,10 @@ impl RefClient {
|
||||
.any(|r| matches!(r, &RecordType::AAAA))
|
||||
.then_some(domain)
|
||||
})
|
||||
.filter(|d| {
|
||||
self.dns_resource_by_domain(d)
|
||||
.is_some_and(|r| r.ip_stack.supports_ipv6())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -1032,7 +1039,7 @@ impl RefClient {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.dns_resource_by_domain(&query.domain)
|
||||
Some(self.dns_resource_by_domain(&query.domain)?.id)
|
||||
}
|
||||
|
||||
pub(crate) fn all_resource_ids(&self) -> Vec<ResourceId> {
|
||||
|
||||
Reference in New Issue
Block a user