Skip to content

Commit 186302d

Browse files
authored
[dns-server] Answers for the rack's zones should be authoritative (#8120)
This makes most answers authoritative and changes tests to match. This won't change rack-internal DNS behavior; we don't rely on `aa` in answers for anything internal. External DNS servers will also now set the `aa` bit for queries for silo domains, though, so our DNS answers will look a bit more normal. We're authoritative for `NXDomain` answers to names under the zones a server is authoritative for that are not present, though we're not (yet) sending an SOA record to indicate how long a client can cache the negative reuslt. We are *not* authoritative for `ServFail` answers even if they are for names we're authoritative for. The `aa` bit here has no RFC-defined meaning, and this is more straightforward than trying to partition `ServFail` into "our names" and "not our names" kinds.
1 parent 3e68262 commit 186302d

File tree

2 files changed

+155
-21
lines changed

2 files changed

+155
-21
lines changed

dns-server/src/dns_server.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,12 @@ async fn handle_dns_message(
281281
let store = &request.store;
282282
debug!(&log, "message_request"; "mr" => #?mr);
283283

284-
let header = Header::response_from_request(mr.header());
284+
let mut header = Header::response_from_request(mr.header());
285+
// We only serve answers for names for which we are authoritative. If we
286+
// bail from this function, our caller will produce their own header and
287+
// have to decide if the error is authoritative.
288+
header.set_authoritative(true);
289+
285290
let query = mr.query();
286291
let name = query.original().name().clone();
287292
let answer = store.query(mr)?;
@@ -430,7 +435,25 @@ async fn respond_nxdomain(
430435
header: &Header,
431436
) {
432437
let log = &request.log;
433-
let mresp = rb_nxdomain.error_msg(&header, ResponseCode::NXDomain);
438+
let mut mresp = rb_nxdomain.error_msg(&header, ResponseCode::NXDomain);
439+
440+
// If we would return NXDOMAIN, the query was for a name in a zone which we
441+
// are authoritative for. So, we set the authoritative bit to confirm in the
442+
// answer that no other server would have records for this name either.
443+
//
444+
// It might make more sense to set the authoritative bit in the caller,
445+
// where we'd know about the conditions from which we are constructing an
446+
// NXDOMAIN. This won't do for a boring reason: `error_msg` above includes
447+
// a `Header::response_from_request`, which discards the authoritative bit
448+
// (and some others, if set). So we must set the authoritative bit on the
449+
// response before encoding and sending it, here.
450+
//
451+
// If we fail to serialize and respond SERVFAIL, the above applies in
452+
// `respond_servfail` as well, and the response will be non-authoritative
453+
// even if we were authoritative for the name. Authoritative SERVFAIL
454+
// doesn't cary any RFC meaning anyway.
455+
mresp.header_mut().set_authoritative(true);
456+
434457
if let Err(error) = encode_and_send(request, mresp, "NXDOMAIN").await {
435458
error!(
436459
log,

dns-server/tests/basic_test.rs

Lines changed: 130 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub async fn a_crud() -> Result<(), anyhow::Error> {
4949

5050
// add an a record
5151
let name = "devron".to_string();
52+
let fqdn = name.clone() + "." + TEST_ZONE + ".";
5253
let addr = Ipv4Addr::new(10, 1, 2, 3);
5354
let a = DnsRecord::A(addr);
5455
let input_records = HashMap::from([(name.clone(), vec![a])]);
@@ -59,10 +60,24 @@ pub async fn a_crud() -> Result<(), anyhow::Error> {
5960
assert_eq!(input_records, records);
6061

6162
// resolve the name
62-
let response = resolver.lookup_ip(name + "." + TEST_ZONE + ".").await?;
63+
let response = resolver.lookup_ip(fqdn.clone()).await?;
6364
let address = response.iter().next().expect("no addresses returned!");
6465
assert_eq!(address, addr);
6566

67+
// as with other cases, `hickory-resolver` does not let us see if the answer
68+
// is authoritative, so we'll have to query again with a lower level
69+
// interface to validate that.
70+
let raw_response = raw_dns_client_query(
71+
test_ctx.dns_server.local_address(),
72+
Name::from_ascii(&fqdn).expect("name is valid"),
73+
RecordType::A,
74+
)
75+
.await
76+
.expect("can issue DNS query");
77+
78+
assert!(raw_response.authoritative());
79+
assert_eq!(raw_response.answers().len(), 1);
80+
6681
test_ctx.cleanup().await;
6782
Ok(())
6883
}
@@ -79,6 +94,7 @@ pub async fn aaaa_crud() -> Result<(), anyhow::Error> {
7994

8095
// add an aaaa record
8196
let name = "devron".to_string();
97+
let fqdn = name.clone() + "." + TEST_ZONE + ".";
8298
let addr = Ipv6Addr::new(0xfd, 0, 0, 0, 0, 0, 0, 0x1);
8399
let aaaa = DnsRecord::Aaaa(addr);
84100
let input_records = HashMap::from([(name.clone(), vec![aaaa])]);
@@ -89,10 +105,24 @@ pub async fn aaaa_crud() -> Result<(), anyhow::Error> {
89105
assert_eq!(input_records, records);
90106

91107
// resolve the name
92-
let response = resolver.lookup_ip(name + "." + TEST_ZONE + ".").await?;
108+
let response = resolver.lookup_ip(fqdn.clone()).await?;
93109
let address = response.iter().next().expect("no addresses returned!");
94110
assert_eq!(address, addr);
95111

112+
// as with other cases, `hickory-resolver` does not let us see if the answer
113+
// is authoritative, so we'll have to query again with a lower level
114+
// interface to validate that.
115+
let raw_response = raw_dns_client_query(
116+
test_ctx.dns_server.local_address(),
117+
Name::from_ascii(&fqdn).expect("name is valid"),
118+
RecordType::AAAA,
119+
)
120+
.await
121+
.expect("can issue DNS query");
122+
123+
assert!(raw_response.authoritative());
124+
assert_eq!(raw_response.answers().len(), 1);
125+
96126
test_ctx.cleanup().await;
97127
Ok(())
98128
}
@@ -128,7 +158,7 @@ pub async fn answers_match_question() -> Result<(), anyhow::Error> {
128158
//
129159
// `raw_dns_client_query` avoids using a hickory Resolver, so we can assert
130160
// on the exact answer from our server.
131-
let response = raw_dns_client_query(
161+
let raw_response = raw_dns_client_query(
132162
test_ctx.dns_server.local_address(),
133163
name,
134164
RecordType::A,
@@ -142,9 +172,12 @@ pub async fn answers_match_question() -> Result<(), anyhow::Error> {
142172
// have
143173
// * no additionals: the server could return AAAA records as additionals to
144174
// an A query, but does not currently.
145-
assert_eq!(response.header().response_code(), ResponseCode::NoError);
146-
assert_eq!(response.answers(), &[]);
147-
assert_eq!(response.additionals(), &[]);
175+
// * authoritative: the nameserver we've queried is the source of truth for
176+
// this name (and zone!)
177+
assert_eq!(raw_response.header().response_code(), ResponseCode::NoError);
178+
assert_eq!(raw_response.answers(), &[]);
179+
assert_eq!(raw_response.additionals(), &[]);
180+
assert!(raw_response.authoritative());
148181

149182
test_ctx.cleanup().await;
150183
Ok(())
@@ -223,6 +256,7 @@ pub async fn srv_crud() -> Result<(), anyhow::Error> {
223256
assert_eq!(response.additionals().len(), 2);
224257
assert_eq!(response.additionals()[0].record_type(), RecordType::AAAA);
225258
assert_eq!(response.additionals()[1].record_type(), RecordType::AAAA);
259+
assert!(response.authoritative());
226260

227261
test_ctx.cleanup().await;
228262
Ok(())
@@ -263,11 +297,8 @@ pub async fn multi_record_crud() -> Result<(), anyhow::Error> {
263297
Ok(())
264298
}
265299

266-
async fn lookup_ip_expect_nxdomain(resolver: &TokioAsyncResolver, name: &str) {
267-
lookup_ip_expect_error_code(resolver, name, ResponseCode::NXDomain).await;
268-
}
269-
270300
async fn lookup_ip_expect_error_code(
301+
server_addr: std::net::SocketAddr,
271302
resolver: &TokioAsyncResolver,
272303
name: &str,
273304
expected_code: ResponseCode,
@@ -278,6 +309,58 @@ async fn lookup_ip_expect_error_code(
278309
}
279310
Err(e) => expect_no_records_error_code(&e, expected_code),
280311
};
312+
313+
// We are authoritative for all records served from internal DNS or external
314+
// DNS. This means that if we return an NXDOMAIN answer, the queried name
315+
// is one that we would be authoritative for, if it existed. For other
316+
// errors, we do not set the authoritative bit even if the name is one we
317+
// might have been authoritative for. This is primarily for simplicity; the
318+
// authoritative non-NXDOMAIN errors have no RFC-defined meaning.
319+
let expected_authoritativeness = expected_code == ResponseCode::NXDomain;
320+
321+
// `lookup_ip` doesn't let us find out if the actual DNS message was
322+
// authoritative. So instead, query again via `hickory-client` to get the
323+
// exact Message from our DNS server. `lookup_ip` queries both A and AAAA
324+
// and merges the answers, so we'll query both here too.
325+
326+
let raw_response =
327+
raw_query_expect_err(server_addr, name, RecordType::A).await;
328+
329+
assert_eq!(raw_response.authoritative(), expected_authoritativeness);
330+
assert_eq!(raw_response.response_code(), expected_code);
331+
332+
let raw_response =
333+
raw_query_expect_err(server_addr, name, RecordType::AAAA).await;
334+
assert_eq!(raw_response.authoritative(), expected_authoritativeness);
335+
assert_eq!(raw_response.response_code(), expected_code);
336+
}
337+
338+
async fn raw_query_expect_err(
339+
server_addr: std::net::SocketAddr,
340+
name: &str,
341+
query_ty: RecordType,
342+
) -> DnsResponse {
343+
let name = Name::from_ascii(name).expect("can parse domain name");
344+
345+
let raw_response = raw_dns_client_query(server_addr, name, query_ty)
346+
.await
347+
.expect("can issue DNS query");
348+
349+
// The caller may have a specific error in mind, but we know that the
350+
// response definitely should be that there was *some* kind of error.
351+
assert_ne!(raw_response.response_code(), ResponseCode::NoError);
352+
353+
// We do not currently return answers or additionals for any errors.
354+
assert!(raw_response.answers().is_empty());
355+
assert!(raw_response.additionals().is_empty());
356+
357+
// Optionally, the DNS server is permitted to return SOA records with
358+
// negative answers as a guide for how long to cache the result. We don't do
359+
// that right now, so test that there are no name servers in the answer.
360+
// This should change if the DNS server is changed.
361+
assert!(raw_response.name_servers().is_empty());
362+
363+
raw_response
281364
}
282365

283366
fn expect_no_records_error_code(
@@ -338,7 +421,13 @@ pub async fn name_contains_zone() -> Result<(), anyhow::Error> {
338421
assert_eq!(address, addr);
339422

340423
// A lookup shouldn't work without the zone's name appended twice.
341-
lookup_ip_expect_nxdomain(resolver, "epsilon3.oxide.test").await;
424+
lookup_ip_expect_error_code(
425+
test_ctx.dns_server.local_address(),
426+
resolver,
427+
"epsilon3.oxide.test",
428+
ResponseCode::NXDomain,
429+
)
430+
.await;
342431

343432
test_ctx.cleanup().await;
344433
Ok(())
@@ -362,7 +451,13 @@ pub async fn empty_record() -> Result<(), anyhow::Error> {
362451
assert!(records.is_empty());
363452

364453
// resolve the name
365-
lookup_ip_expect_nxdomain(&resolver, &(name + "." + TEST_ZONE + ".")).await;
454+
lookup_ip_expect_error_code(
455+
test_ctx.dns_server.local_address(),
456+
&resolver,
457+
&(name + "." + TEST_ZONE + "."),
458+
ResponseCode::NXDomain,
459+
)
460+
.await;
366461

367462
test_ctx.cleanup().await;
368463
Ok(())
@@ -450,15 +545,24 @@ pub async fn soa() -> Result<(), anyhow::Error> {
450545

451546
// SOA queries under the zone we now know we are authoritative for should
452547
// fail with NXDomain.
453-
//
454-
// TODO: we should see the authoritative bit set here. It's not clear that
455-
// hickory-proto has a way to see if that bit is present in an error.
548+
let no_soa_name = format!("foo.{TEST_ZONE}.");
456549
let lookup_err = resolver
457-
.soa_lookup(format!("foo.{TEST_ZONE}."))
550+
.soa_lookup(&no_soa_name)
458551
.await
459552
.expect_err("test zone should not exist");
460553
expect_no_records_error_code(&lookup_err, ResponseCode::NXDomain);
461554

555+
// As with other NXDomain answers, we should see the authoritative bit.
556+
let raw_response = raw_query_expect_err(
557+
test_ctx.dns_server.local_address(),
558+
&no_soa_name,
559+
RecordType::A,
560+
)
561+
.await;
562+
563+
assert!(raw_response.authoritative());
564+
assert_eq!(raw_response.response_code(), ResponseCode::NXDomain);
565+
462566
// If the zone has no ns1 for some reason, we ought to see an SOA record
463567
// referencing the next lowest-numbered name server.
464568
let mut records = HashMap::new();
@@ -500,8 +604,13 @@ pub async fn nxdomain() -> Result<(), anyhow::Error> {
500604

501605
// asking for a nonexistent record within the domain of the internal DNS
502606
// server should result in an NXDOMAIN
503-
lookup_ip_expect_nxdomain(&resolver, &format!("unicorn.{}.", TEST_ZONE))
504-
.await;
607+
lookup_ip_expect_error_code(
608+
test_ctx.dns_server.local_address(),
609+
&resolver,
610+
&format!("unicorn.{}.", TEST_ZONE),
611+
ResponseCode::NXDomain,
612+
)
613+
.await;
505614

506615
test_ctx.cleanup().await;
507616
Ok(())
@@ -514,8 +623,10 @@ pub async fn servfail() -> Result<(), anyhow::Error> {
514623

515624
// In this case, we haven't defined any zones yet, so any request should be
516625
// outside the server's authoritative zones. That should result in a
517-
// SERVFAIL.
626+
// SERVFAIL. Further, `lookup_ip_expect_error_code` will check that the
627+
// error is not authoritative.
518628
lookup_ip_expect_error_code(
629+
test_ctx.dns_server.local_address(),
519630
&resolver,
520631
"unicorn.oxide.internal",
521632
ResponseCode::ServFail,

0 commit comments

Comments
 (0)