2
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
5
+ use hickory_resolver:: ResolveError as HickoryResolveError ;
6
+ use hickory_resolver:: ResolveErrorKind as HickoryResolveErrorKind ;
5
7
use hickory_resolver:: TokioResolver ;
6
8
use hickory_resolver:: config:: {
7
9
LookupIpStrategy , NameServerConfig , ResolveHosts , ResolverConfig ,
8
10
ResolverOpts ,
9
11
} ;
10
12
use hickory_resolver:: lookup:: SrvLookup ;
11
13
use hickory_resolver:: name_server:: TokioConnectionProvider ;
12
- use internal_dns_types:: names:: ServiceName ;
14
+ use internal_dns_types:: names:: { DNS_ZONE , ServiceName } ;
13
15
use omicron_common:: address:: {
14
16
AZ_PREFIX , DNS_PORT , Ipv6Subnet , get_internal_dns_server_addresses,
15
17
} ;
18
+ use omicron_uuid_kinds:: OmicronZoneUuid ;
16
19
use slog:: { debug, error, info, trace} ;
17
20
use std:: net:: { Ipv6Addr , SocketAddr , SocketAddrV6 } ;
18
21
@@ -28,6 +31,37 @@ pub enum ResolveError {
28
31
NotFoundByString ( String ) ,
29
32
}
30
33
34
+ fn is_no_records_found ( err : & hickory_resolver:: ResolveError ) -> bool {
35
+ match err. kind ( ) {
36
+ hickory_resolver:: ResolveErrorKind :: Proto ( proto_error) => {
37
+ match proto_error. kind ( ) {
38
+ hickory_resolver:: proto:: ProtoErrorKind :: NoRecordsFound {
39
+ ..
40
+ } => true ,
41
+ _ => false ,
42
+ }
43
+ }
44
+ _ => false ,
45
+ }
46
+ }
47
+
48
+ impl ResolveError {
49
+ /// Returns "true" if this error indicates the record is not found.
50
+ pub fn is_not_found ( & self ) -> bool {
51
+ match self {
52
+ ResolveError :: NotFound ( _) | ResolveError :: NotFoundByString ( _) => {
53
+ true
54
+ }
55
+ ResolveError :: Resolve ( hickory_err)
56
+ if is_no_records_found ( & hickory_err) =>
57
+ {
58
+ true
59
+ }
60
+ _ => false ,
61
+ }
62
+ }
63
+ }
64
+
31
65
/// A wrapper around a set of bootstrap DNS addresses, providing a convenient
32
66
/// way to construct a [`qorb::resolvers::dns::DnsResolver`] for specific
33
67
/// services.
@@ -314,6 +348,40 @@ impl Resolver {
314
348
}
315
349
}
316
350
351
+ /// Returns the targets of the SRV records for a DNS name with their
352
+ /// associated zone UUIDs.
353
+ ///
354
+ /// Similar to [`Resolver::lookup_all_socket_v6`], but extracts the
355
+ /// OmicronZoneUuid from DNS target names that follow the pattern
356
+ /// `{uuid}.host.{DNS_ZONE}`. Returns a list of (OmicronZoneUuid,
357
+ /// SocketAddrV6) pairs.
358
+ ///
359
+ /// Returns an error if any target cannot be parsed as a zone UUID pattern.
360
+ pub async fn lookup_all_socket_and_zone_v6 (
361
+ & self ,
362
+ service : ServiceName ,
363
+ ) -> Result < Vec < ( OmicronZoneUuid , SocketAddrV6 ) > , ResolveError > {
364
+ let name = service. srv_name ( ) ;
365
+ trace ! ( self . log, "lookup_all_socket_and_zone_v6 srv" ; "dns_name" => & name) ;
366
+ let response = self . resolver . srv_lookup ( & name) . await ?;
367
+ debug ! (
368
+ self . log,
369
+ "lookup_all_socket_and_zone_v6 srv" ;
370
+ "dns_name" => & name,
371
+ "response" => ?response
372
+ ) ;
373
+
374
+ let results = self
375
+ . lookup_service_targets_with_zones ( response)
376
+ . await ?
377
+ . collect :: < Vec < _ > > ( ) ;
378
+ if !results. is_empty ( ) {
379
+ Ok ( results)
380
+ } else {
381
+ Err ( ResolveError :: NotFound ( service) )
382
+ }
383
+ }
384
+
317
385
// Returns an iterator of SocketAddrs for the specified SRV name.
318
386
//
319
387
// Acts on a raw string for compatibility with the reqwest::dns::Resolve
@@ -399,6 +467,99 @@ impl Resolver {
399
467
. flatten ( )
400
468
}
401
469
470
+ /// Similar to [`Resolver::lookup_service_targets`], but extracts zone UUIDs from target names.
471
+ ///
472
+ /// Returns an iterator of (OmicronZoneUuid, SocketAddrV6) pairs for targets that match
473
+ /// the pattern `{uuid}.host.{DNS_ZONE}`. Returns an error if any target doesn't match
474
+ /// this pattern.
475
+ async fn lookup_service_targets_with_zones (
476
+ & self ,
477
+ service_lookup : SrvLookup ,
478
+ ) -> Result <
479
+ impl Iterator < Item = ( OmicronZoneUuid , SocketAddrV6 ) > + Send ,
480
+ ResolveError ,
481
+ > {
482
+ let futures =
483
+ std:: iter:: repeat ( ( self . log . clone ( ) , self . resolver . clone ( ) ) )
484
+ . zip ( service_lookup. into_iter ( ) )
485
+ . map ( |( ( log, resolver) , srv) | async move {
486
+ let target = srv. target ( ) ;
487
+ let port = srv. port ( ) ;
488
+ let target_str = target. to_string ( ) ;
489
+ // Try to parse the zone UUID from the target name
490
+ let zone_uuid = match Self :: parse_zone_uuid_from_target ( & target_str) {
491
+ Some ( uuid) => uuid,
492
+ None => {
493
+ error ! (
494
+ log,
495
+ "lookup_service_targets_with_zones: target doesn't match zone pattern" ;
496
+ "target" => ?target_str,
497
+ ) ;
498
+ return Err ( (
499
+ target. clone ( ) ,
500
+ HickoryResolveError :: from (
501
+ HickoryResolveErrorKind :: Message (
502
+ "target doesn't match zone pattern"
503
+ )
504
+ )
505
+ ) ) ;
506
+ }
507
+ } ;
508
+ trace ! (
509
+ log,
510
+ "lookup_service_targets_with_zones: looking up SRV target" ;
511
+ "name" => ?target,
512
+ "zone_uuid" => ?zone_uuid,
513
+ ) ;
514
+ resolver
515
+ . ipv6_lookup ( target. clone ( ) )
516
+ . await
517
+ . map ( |ips| ( ips, port, zone_uuid) )
518
+ . map_err ( |err| ( target. clone ( ) , err) )
519
+ } ) ;
520
+ let log = self . log . clone ( ) ;
521
+ let results = futures:: future:: join_all ( futures) . await ;
522
+ let mut socket_addrs = Vec :: new ( ) ;
523
+ for result in results {
524
+ match result {
525
+ Ok ( ( ips, port, zone_uuid) ) => {
526
+ // Add all IP addresses for this zone
527
+ for aaaa in ips {
528
+ socket_addrs. push ( (
529
+ zone_uuid,
530
+ SocketAddrV6 :: new ( aaaa. into ( ) , port, 0 , 0 ) ,
531
+ ) ) ;
532
+ }
533
+ }
534
+ Err ( ( target, err) ) => {
535
+ error ! (
536
+ log,
537
+ "lookup_service_targets_with_zones: failed looking up target" ;
538
+ "name" => ?target,
539
+ "error" => ?err,
540
+ ) ;
541
+ return Err ( ResolveError :: Resolve ( err) ) ;
542
+ }
543
+ }
544
+ }
545
+ Ok ( socket_addrs. into_iter ( ) )
546
+ }
547
+
548
+ /// Parse a zone UUID from a DNS target name following the pattern `{uuid}.host.{DNS_ZONE}`.
549
+ fn parse_zone_uuid_from_target ( target : & str ) -> Option < OmicronZoneUuid > {
550
+ // Remove trailing dot if present
551
+ let target = target. strip_suffix ( '.' ) . unwrap_or ( target) ;
552
+
553
+ // Expected format: "{uuid}.host.{DNS_ZONE}"
554
+ let expected_suffix = format ! ( ".host.{}" , DNS_ZONE ) ;
555
+
556
+ if let Some ( uuid_str) = target. strip_suffix ( & expected_suffix) {
557
+ uuid_str. parse :: < OmicronZoneUuid > ( ) . ok ( )
558
+ } else {
559
+ None
560
+ }
561
+ }
562
+
402
563
/// Lookup a specific record's IPv6 address
403
564
///
404
565
/// In general, callers should _not_ be using this function, and instead
@@ -436,7 +597,7 @@ mod test {
436
597
use internal_dns_types:: names:: DNS_ZONE ;
437
598
use internal_dns_types:: names:: ServiceName ;
438
599
use omicron_test_utils:: dev:: test_setup_log;
439
- use omicron_uuid_kinds:: OmicronZoneUuid ;
600
+ use omicron_uuid_kinds:: { OmicronZoneUuid , SledUuid } ;
440
601
use slog:: { Logger , o} ;
441
602
use std:: collections:: HashMap ;
442
603
use std:: net:: Ipv6Addr ;
@@ -1131,4 +1292,77 @@ mod test {
1131
1292
dns_server. cleanup_successful ( ) ;
1132
1293
logctx. cleanup_successful ( ) ;
1133
1294
}
1295
+
1296
+ #[ tokio:: test]
1297
+ async fn lookup_all_socket_and_zone_v6_success_and_failure ( ) {
1298
+ let logctx =
1299
+ test_setup_log ( "lookup_all_socket_and_zone_v6_success_and_failure" ) ;
1300
+ let dns_server = DnsServer :: create ( & logctx. log ) . await ;
1301
+ let resolver = dns_server. resolver ( ) . unwrap ( ) ;
1302
+
1303
+ // Create DNS config with both zone and sled services
1304
+ let mut dns_config = DnsConfigBuilder :: new ( ) ;
1305
+
1306
+ // Add a zone service (BoundaryNtp) that should succeed
1307
+ let zone_uuid = OmicronZoneUuid :: new_v4 ( ) ;
1308
+ let zone_ip = Ipv6Addr :: new ( 0xfd , 0 , 0 , 0 , 0 , 0 , 0 , 0x1 ) ;
1309
+ let zone_port = 8080 ;
1310
+ let zone_host = dns_config. host_zone ( zone_uuid, zone_ip) . unwrap ( ) ;
1311
+ dns_config
1312
+ . service_backend_zone (
1313
+ ServiceName :: BoundaryNtp ,
1314
+ & zone_host,
1315
+ zone_port,
1316
+ )
1317
+ . unwrap ( ) ;
1318
+
1319
+ // Add a sled service (SledAgent) that should fail
1320
+ let sled_uuid = SledUuid :: new_v4 ( ) ;
1321
+ let sled_ip = Ipv6Addr :: new ( 0xfd , 0 , 0 , 0 , 0 , 0 , 0 , 0x2 ) ;
1322
+ let sled_port = 8081 ;
1323
+ let sled_host = dns_config. host_sled ( sled_uuid, sled_ip) . unwrap ( ) ;
1324
+ dns_config
1325
+ . service_backend_sled (
1326
+ ServiceName :: SledAgent ( sled_uuid) ,
1327
+ & sled_host,
1328
+ sled_port,
1329
+ )
1330
+ . unwrap ( ) ;
1331
+
1332
+ let dns_config = dns_config. build_full_config_for_initial_generation ( ) ;
1333
+ dns_server. update ( & dns_config) . await . unwrap ( ) ;
1334
+
1335
+ // Test 1: Zone service should succeed
1336
+ let zone_results = resolver
1337
+ . lookup_all_socket_and_zone_v6 ( ServiceName :: BoundaryNtp )
1338
+ . await
1339
+ . expect ( "Should have been able to look up zone service" ) ;
1340
+
1341
+ assert_eq ! ( zone_results. len( ) , 1 ) ;
1342
+ let ( returned_zone_uuid, returned_addr) = & zone_results[ 0 ] ;
1343
+ assert_eq ! ( * returned_zone_uuid, zone_uuid) ;
1344
+ assert_eq ! ( returned_addr. ip( ) , & zone_ip) ;
1345
+ assert_eq ! ( returned_addr. port( ) , zone_port) ;
1346
+
1347
+ // Test 2: Sled service should fail (targets don't match zone pattern)
1348
+ let sled_error = resolver
1349
+ . lookup_all_socket_and_zone_v6 ( ServiceName :: SledAgent ( sled_uuid) )
1350
+ . await
1351
+ . expect_err ( "Should have failed to look up sled service" ) ;
1352
+
1353
+ // The error should be a ResolveError indicating the target doesn't match the zone pattern
1354
+ match sled_error {
1355
+ ResolveError :: Resolve ( hickory_err) => {
1356
+ assert ! (
1357
+ hickory_err
1358
+ . to_string( )
1359
+ . contains( "target doesn't match zone pattern" )
1360
+ ) ;
1361
+ }
1362
+ _ => panic ! ( "Expected ResolveError::Resolve, got {:?}" , sled_error) ,
1363
+ }
1364
+
1365
+ dns_server. cleanup_successful ( ) ;
1366
+ logctx. cleanup_successful ( ) ;
1367
+ }
1134
1368
}
0 commit comments