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