Skip to content

Commit 2066bd5

Browse files
authored
internal-dns: return additional records for srv record targets (#3785)
It's not required but recommended for a DNS server to return the address record(s) in the "Additional" section when a responding to a lookup for a `SRV` records. This saves the client from doing additional lookups. Unfortunately, we still do need to do the explicit lookup on targets due to deficiencies in `trust-dns`. Fixes #3434 Before: ``` $ dig @fd00:1122:3344:1::1 SRV _cockroach._tcp.control-plane.oxide.internal ; <<>> DiG 9.18.14 <<>> @fd00:1122:3344:1::1 SRV _cockroach._tcp.control-plane.oxide.internal ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13647 ;; flags: qr rd; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0 ;; WARNING: recursion requested but not available ;; QUESTION SECTION: ;_cockroach._tcp.control-plane.oxide.internal. IN SRV ;; ANSWER SECTION: _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 65dd2374-f712-4496-b0ff-11a599fa0c2c.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 b512e7a3-b115-4150-b220-3bf095118cb5.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 b9b20bbf-d0c1-474d-9605-56b2b23a4b5f.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 bedb84f6-3d51-4b21-931a-0b02b326e75a.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 c7d408e5-b1c9-42ea-862f-ce2639a56978.host.control-plane.oxide.internal. ;; Query time: 17 msec ;; SERVER: fd00:1122:3344:1::1#53(fd00:1122:3344:1::1) (UDP) ;; WHEN: Fri Jul 28 00:28:33 UTC 2023 ;; MSG SIZE rcvd: 512 ``` After: ``` $ dig @fd00:1122:3344:1::1 SRV _cockroach._tcp.control-plane.oxide.internal ; <<>> DiG 9.18.14 <<>> @fd00:1122:3344:1::1 SRV _cockroach._tcp.control-plane.oxide.internal ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44496 ;; flags: qr rd; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 5 ;; WARNING: recursion requested but not available ;; QUESTION SECTION: ;_cockroach._tcp.control-plane.oxide.internal. IN SRV ;; ANSWER SECTION: _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 148c38a0-294e-436a-8ba0-e3a0eb166162.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 6afab2d4-ac54-4bfb-96a8-fbbeabb75247.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 7d07596f-d69e-4335-b0eb-d27fbe8d092f.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 9f339f78-4b69-46a9-9f16-8239152a413a.host.control-plane.oxide.internal. _cockroach._tcp.control-plane.oxide.internal. 0 IN SRV 0 0 32221 a752818c-c324-4c73-a5bb-a4366ea05ec0.host.control-plane.oxide.internal. ;; ADDITIONAL SECTION: 148c38a0-294e-436a-8ba0-e3a0eb166162.host.control-plane.oxide.internal. 0 IN AAAA fd00:1122:3344:101::5 6afab2d4-ac54-4bfb-96a8-fbbeabb75247.host.control-plane.oxide.internal. 0 IN AAAA fd00:1122:3344:101::3 7d07596f-d69e-4335-b0eb-d27fbe8d092f.host.control-plane.oxide.internal. 0 IN AAAA fd00:1122:3344:101::6 9f339f78-4b69-46a9-9f16-8239152a413a.host.control-plane.oxide.internal. 0 IN AAAA fd00:1122:3344:101::4 a752818c-c324-4c73-a5bb-a4366ea05ec0.host.control-plane.oxide.internal. 0 IN AAAA fd00:1122:3344:101::7 ;; Query time: 4 msec ;; SERVER: fd00:1122:3344:1::1#53(fd00:1122:3344:1::1) (UDP) ;; WHEN: Thu Jul 27 16:39:20 PDT 2023 ;; MSG SIZE rcvd: 652 ```
1 parent 3d70776 commit 2066bd5

File tree

5 files changed

+394
-163
lines changed

5 files changed

+394
-163
lines changed

dns-server/src/dns_server.rs

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,49 @@ impl From<QueryError> for RequestError {
213213
}
214214
}
215215

216+
fn dns_record_to_record(
217+
name: &Name,
218+
record: DnsRecord,
219+
) -> Result<Record, RequestError> {
220+
match record {
221+
DnsRecord::A(addr) => {
222+
let mut a = Record::new();
223+
a.set_name(name.clone())
224+
.set_rr_type(RecordType::A)
225+
.set_data(Some(RData::A(addr)));
226+
Ok(a)
227+
}
228+
229+
DnsRecord::AAAA(addr) => {
230+
let mut aaaa = Record::new();
231+
aaaa.set_name(name.clone())
232+
.set_rr_type(RecordType::AAAA)
233+
.set_data(Some(RData::AAAA(addr)));
234+
Ok(aaaa)
235+
}
236+
237+
DnsRecord::SRV(crate::dns_types::SRV {
238+
prio,
239+
weight,
240+
port,
241+
target,
242+
}) => {
243+
let tgt = Name::from_str(&target).map_err(|error| {
244+
RequestError::ServFail(anyhow!(
245+
"serialization failed due to bad SRV target {:?}: {:#}",
246+
&target,
247+
error
248+
))
249+
})?;
250+
let mut srv = Record::new();
251+
srv.set_name(name.clone())
252+
.set_rr_type(RecordType::SRV)
253+
.set_data(Some(RData::SRV(SRV::new(prio, weight, port, tgt))));
254+
Ok(srv)
255+
}
256+
}
257+
}
258+
216259
/// Handle a well-formed, decoded DNS query
217260
async fn handle_dns_message(
218261
request: &Request,
@@ -227,55 +270,70 @@ async fn handle_dns_message(
227270
let name = query.original().name().clone();
228271
let records = store.query(mr)?;
229272
let rb = MessageResponseBuilder::from_message_request(mr);
273+
let mut additional_records = vec![];
230274
let response_records = records
231275
.into_iter()
232-
.map(|record| match record {
233-
DnsRecord::A(addr) => {
234-
let mut a = Record::new();
235-
a.set_name(name.clone())
236-
.set_rr_type(RecordType::A)
237-
.set_data(Some(RData::A(addr)));
238-
Ok(a)
239-
}
240-
241-
DnsRecord::AAAA(addr) => {
242-
let mut aaaa = Record::new();
243-
aaaa.set_name(name.clone())
244-
.set_rr_type(RecordType::AAAA)
245-
.set_data(Some(RData::AAAA(addr)));
246-
Ok(aaaa)
247-
}
248-
249-
DnsRecord::SRV(crate::dns_types::SRV {
250-
prio,
251-
weight,
252-
port,
253-
target,
254-
}) => {
255-
let tgt = Name::from_str(&target).map_err(|error| {
256-
RequestError::ServFail(anyhow!(
257-
"serialization failed due to bad SRV target {:?}: {:#}",
258-
&target,
259-
error
260-
))
261-
})?;
262-
let mut srv = Record::new();
263-
srv.set_name(name.clone())
264-
.set_rr_type(RecordType::SRV)
265-
.set_data(Some(RData::SRV(SRV::new(
266-
prio, weight, port, tgt,
267-
))));
268-
Ok(srv)
276+
.map(|record| {
277+
let record = dns_record_to_record(&name, record)?;
278+
279+
// DNS allows for the server to return additional records
280+
// that weren't explicitly asked for by the client but that
281+
// the server expects the client will want. The records
282+
// corresponding to a lookup on a SRV target is one such case.
283+
// We opportunistically attempt to resolve the target here
284+
// and if successful return those additional records in the
285+
// response.
286+
// NOTE: we only do this one-layer deep.
287+
if let Some(RData::SRV(srv)) = record.data() {
288+
let target_records =
289+
store.query_name(srv.target()).map(|records| {
290+
records
291+
.into_iter()
292+
.map(|record| {
293+
dns_record_to_record(srv.target(), record)
294+
})
295+
.collect::<Result<Vec<_>, _>>()
296+
});
297+
match target_records {
298+
Ok(Ok(target_records)) => {
299+
additional_records.extend(target_records);
300+
}
301+
// Don't bail out if we failed to lookup or
302+
// handle the response as the original request
303+
// did succeed and we only care to do this on
304+
// a best-effort basis.
305+
Err(error) => {
306+
slog::warn!(
307+
&log,
308+
"SRV target lookup failed";
309+
"original_mr" => #?mr,
310+
"target" => ?srv.target(),
311+
"error" => ?error,
312+
);
313+
}
314+
Ok(Err(error)) => {
315+
slog::warn!(
316+
&log,
317+
"SRV target unexpected response";
318+
"original_mr" => #?mr,
319+
"target" => ?srv.target(),
320+
"error" => ?error,
321+
);
322+
}
323+
}
269324
}
325+
Ok(record)
270326
})
271327
.collect::<Result<Vec<_>, RequestError>>()?;
272328
debug!(
273329
&log,
274330
"dns response";
275331
"query" => ?query,
276-
"records" => ?&response_records
332+
"records" => ?&response_records,
333+
"additional_records" => ?&additional_records,
277334
);
278-
respond_records(request, rb, header, &response_records).await
335+
respond_records(request, rb, header, &response_records, &additional_records)
336+
.await
279337
}
280338

281339
/// Respond to a DNS query with the given set of DNS records
@@ -284,13 +342,14 @@ async fn respond_records(
284342
rb: MessageResponseBuilder<'_>,
285343
header: Header,
286344
response_records: &[Record],
345+
additional_records: &[Record],
287346
) -> Result<(), RequestError> {
288347
let mresp = rb.build(
289348
header,
290349
response_records.iter().collect::<Vec<&Record>>(),
291350
vec![],
292351
vec![],
293-
vec![],
352+
additional_records,
294353
);
295354

296355
encode_and_send(&request, mresp, "records").await.map_err(|error| {

dns-server/src/storage.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -590,10 +590,20 @@ impl Store {
590590
) -> Result<Vec<DnsRecord>, QueryError> {
591591
let name = mr.query().name();
592592
let orig_name = mr.query().original().name();
593-
self.query_name(name, orig_name)
593+
self.query_raw(name, orig_name)
594594
}
595595

596-
fn query_name(
596+
/// Returns a non-empty list of DNS records associated with the given name.
597+
///
598+
/// If the returned set would have been empty, returns `QueryError::NoName`.
599+
pub(crate) fn query_name(
600+
&self,
601+
name: &Name,
602+
) -> Result<Vec<DnsRecord>, QueryError> {
603+
self.query_raw(&LowerName::new(name), name)
604+
}
605+
606+
fn query_raw(
597607
&self,
598608
name: &LowerName,
599609
orig_name: &Name,
@@ -843,7 +853,7 @@ mod test {
843853
fn expect(store: &Store, name: &str, expect: Expect<'_>) {
844854
let dns_name_orig = Name::from_str(name).expect("bad DNS name");
845855
let dns_name_lower = LowerName::from(dns_name_orig.clone());
846-
let result = store.query_name(&dns_name_lower, &dns_name_orig);
856+
let result = store.query_raw(&dns_name_lower, &dns_name_orig);
847857
println!(
848858
"expecting {:?} for query of {:?}: {:?}",
849859
expect, name, result

dns-server/tests/basic_test.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ use dns_service_client::{
1010
use dropshot::{test_util::LogContext, HandlerTaskMode};
1111
use omicron_test_utils::dev::test_setup_log;
1212
use slog::o;
13-
use std::{collections::HashMap, net::Ipv4Addr, net::Ipv6Addr};
13+
use std::{
14+
collections::HashMap,
15+
net::Ipv6Addr,
16+
net::{IpAddr, Ipv4Addr},
17+
};
1418
use trust_dns_resolver::error::ResolveErrorKind;
1519
use trust_dns_resolver::TokioAsyncResolver;
1620
use trust_dns_resolver::{
@@ -92,8 +96,13 @@ pub async fn srv_crud() -> Result<(), anyhow::Error> {
9296

9397
// add a srv record
9498
let name = "hromi".to_string();
95-
let srv =
96-
Srv { prio: 47, weight: 74, port: 99, target: "outpost47".into() };
99+
let target = "outpost47";
100+
let srv = Srv {
101+
prio: 47,
102+
weight: 74,
103+
port: 99,
104+
target: format!("{target}.{TEST_ZONE}"),
105+
};
97106
let rec = DnsRecord::Srv(srv.clone());
98107
let input_records = HashMap::from([(name.clone(), vec![rec])]);
99108
dns_records_create(client, TEST_ZONE, input_records.clone()).await?;
@@ -102,13 +111,25 @@ pub async fn srv_crud() -> Result<(), anyhow::Error> {
102111
let records = dns_records_list(client, TEST_ZONE).await?;
103112
assert_eq!(records, input_records);
104113

114+
// add some aaaa records corresponding to the srv target
115+
let addr1 = Ipv6Addr::new(0xfd, 0, 0, 0, 0, 0, 0, 0x1);
116+
let addr2 = Ipv6Addr::new(0xfd, 0, 0, 0, 0, 0, 0, 0x2);
117+
let input_records = HashMap::from([(
118+
target.to_string(),
119+
vec![DnsRecord::Aaaa(addr1), DnsRecord::Aaaa(addr2)],
120+
)]);
121+
dns_records_create(client, TEST_ZONE, input_records.clone()).await?;
122+
105123
// resolve the srv
106124
let response = resolver.srv_lookup(name + "." + TEST_ZONE + ".").await?;
107-
let srvr = response.iter().next().expect("no addresses returned!");
125+
let srvr = response.iter().next().expect("no srv records returned!");
108126
assert_eq!(srvr.priority(), srv.prio);
109127
assert_eq!(srvr.weight(), srv.weight);
110128
assert_eq!(srvr.port(), srv.port);
111129
assert_eq!(srvr.target().to_string(), srv.target + ".");
130+
let mut aaaa_records = response.ip_iter().collect::<Vec<_>>();
131+
aaaa_records.sort();
132+
assert_eq!(aaaa_records, [IpAddr::from(addr1), IpAddr::from(addr2)]);
112133

113134
test_ctx.cleanup().await;
114135
Ok(())

internal-dns/src/config.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ use std::net::Ipv6Addr;
6868
use uuid::Uuid;
6969

7070
/// Zones that can be referenced within the internal DNS system.
71-
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
71+
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
7272
pub enum ZoneVariant {
7373
/// This non-global zone runs an instance of Dendrite.
7474
///
@@ -164,6 +164,12 @@ pub struct Zone {
164164
variant: ZoneVariant,
165165
}
166166

167+
impl Zone {
168+
pub(crate) fn dns_name(&self) -> String {
169+
Host::Zone { id: self.id, variant: self.variant }.dns_name()
170+
}
171+
}
172+
167173
impl DnsConfigBuilder {
168174
pub fn new() -> Self {
169175
DnsConfigBuilder {
@@ -342,9 +348,7 @@ impl DnsConfigBuilder {
342348

343349
// Assemble the set of AAAA records for zones.
344350
let zone_records = self.zones.into_iter().map(|(zone, zone_ip)| {
345-
let name =
346-
Host::Zone { id: zone.id, variant: zone.variant }.dns_name();
347-
(name, vec![DnsRecord::Aaaa(zone_ip)])
351+
(zone.dns_name(), vec![DnsRecord::Aaaa(zone_ip)])
348352
});
349353

350354
// Assemble the set of SRV records, which implicitly point back at
@@ -359,15 +363,7 @@ impl DnsConfigBuilder {
359363
prio: 0,
360364
weight: 0,
361365
port,
362-
target: format!(
363-
"{}.{}",
364-
Host::Zone {
365-
id: zone.id,
366-
variant: zone.variant
367-
}
368-
.dns_name(),
369-
DNS_ZONE
370-
),
366+
target: format!("{}.{}", zone.dns_name(), DNS_ZONE),
371367
})
372368
})
373369
.collect();

0 commit comments

Comments
 (0)