@@ -11,7 +11,11 @@ cfg_if::cfg_if! {
1111 use std:: ptr:: { null_mut} ;
1212 use core:: ffi:: { c_void} ;
1313
14- pub ( crate ) fn dns_query_srv_records( name: & str ) -> Vec <String > {
14+ /// Queries SRV records for `name`, returning each `(target, port)`.
15+ ///
16+ /// `DnsQuery_W` yields a linked list that may include non-SRV records (e.g. A records for
17+ /// the SRV targets), so we walk `pNext` and keep only SRV entries.
18+ pub ( crate ) fn dns_query_srv_records( name: & str ) -> Vec <( String , u16 ) > {
1519 let mut records = Vec :: new( ) ;
1620
1721 let mut p_query_results: * mut DNS_RECORDA = null_mut( ) ;
@@ -30,23 +34,29 @@ cfg_if::cfg_if! {
3034
3135 match dns_status. ok( ) {
3236 Ok ( ( ) ) => {
33- // SAFETY: `p_query_results` in non-null because `dns_status` is Ok.
34- let query_results = unsafe { * p_query_results } ;
35- // SAFETY: `query_results.Data` is guaranteed to contain `SRV` because
36- // of arguments to `DnsQuery_W`.
37- let p_name_target = unsafe { query_results. Data . Srv . pNameTarget } ;
38- let name_target = PWSTR :: from_raw( p_name_target. as_ptr( ) as * mut u16 ) ;
39- // SAFETY: `name_target` is guaranteed to be correct at this point.
40- let name_target = unsafe { name_target. to_string( ) } ;
41-
42- if let Ok ( name_target) = name_target {
43- records. push( name_target) ;
37+ let mut p_record = p_query_results;
38+ while !p_record. is_null( ) {
39+ // SAFETY: `p_record` is non-null and points to a valid `DNS_RECORDA`.
40+ let record = unsafe { * p_record } ;
41+
42+ if record. wType == DNS_TYPE_SRV . 0 {
43+ // SAFETY: `wType == DNS_TYPE_SRV` guarantees the `Srv` union member is active.
44+ let srv = unsafe { record. Data . Srv } ;
45+ // `DnsQuery_W` returns wide strings even in the `*A` record struct.
46+ let name_target = PWSTR :: from_raw( srv. pNameTarget. as_ptr( ) as * mut u16 ) ;
47+ // SAFETY: `name_target` points to a valid, null-terminated UTF-16 string.
48+ if let Ok ( name_target) = unsafe { name_target. to_string( ) } {
49+ records. push( ( name_target, srv. wPort) ) ;
50+ }
51+ }
52+
53+ p_record = record. pNext;
4454 }
4555 }
4656 Err ( error) => error!( %error, "DnsQuery_W failed" ) ,
4757 }
4858
49- // SAFETY: `p_query_results` is not null.
59+ // SAFETY: `DnsFree` tolerates a null pointer .
5060 unsafe {
5161 DnsFree ( Some ( p_query_results as * const c_void) , DnsFreeRecordList ) ;
5262 }
@@ -136,14 +146,14 @@ cfg_if::cfg_if! {
136146 let krb_tcp_srv = dns_query_srv_records( krb_tcp_name) ;
137147
138148 if !krb_tcp_srv. is_empty( ) {
139- return krb_tcp_srv . iter ( ) . map ( |x| format! ( "tcp://{x}:88" ) ) . collect ( )
149+ return srv_records_to_kdc_urls ( "tcp" , & krb_tcp_srv )
140150 }
141151
142152 let krb_udp_name = & format!( "_kerberos._udp.{domain}" ) ;
143153 let krb_udp_srv = dns_query_srv_records( krb_udp_name) ;
144154
145155 if !krb_udp_srv. is_empty( ) {
146- return krb_udp_srv . iter ( ) . map ( |x| format! ( "udp://{x}:88" ) ) . collect ( )
156+ return srv_records_to_kdc_urls ( "udp" , & krb_udp_srv )
147157 }
148158
149159 Vec :: new( )
@@ -239,14 +249,16 @@ cfg_if::cfg_if! {
239249 let krb_tcp_srv = dns_query_srv_records( krb_tcp_name) ;
240250
241251 if !krb_tcp_srv. is_empty( ) {
242- return krb_tcp_srv. iter( ) . map( |x| format!( "tcp://{}:{}" , & x. target, x. port) ) . collect( )
252+ let records: Vec <( String , u16 ) > = krb_tcp_srv. into_iter( ) . map( |x| ( x. target, x. port) ) . collect( ) ;
253+ return srv_records_to_kdc_urls( "tcp" , & records)
243254 }
244255
245256 let krb_udp_name = & format!( "_kerberos._udp.{domain}" ) ;
246257 let krb_udp_srv = dns_query_srv_records( krb_udp_name) ;
247258
248259 if !krb_udp_srv. is_empty( ) {
249- return krb_udp_srv. iter( ) . map( |x| format!( "udp://{}:{}" , & x. target, x. port) ) . collect( )
260+ let records: Vec <( String , u16 ) > = krb_udp_srv. into_iter( ) . map( |x| ( x. target, x. port) ) . collect( ) ;
261+ return srv_records_to_kdc_urls( "udp" , & records)
250262 }
251263
252264 Vec :: new( )
@@ -341,23 +353,15 @@ cfg_if::cfg_if! {
341353
342354 if let Some ( resolver) = get_dns_resolver( domain) {
343355 if let Ok ( records) = execute_future( resolver. srv_lookup( format!( "_kerberos._tcp.{domain}" ) ) ) {
344- for record in records {
345- let port = record. port( ) ;
346- let target_name = record. target( ) . to_string( ) ;
347- let target_name = target_name. trim_end_matches( '.' ) . to_string( ) ;
348- let kdc_host = format!( "tcp://{}:{}" , & target_name, port) ;
349- kdc_hosts. push( kdc_host) ;
350- }
356+ let records: Vec <( String , u16 ) > =
357+ records. into_iter( ) . map( |r| ( r. target( ) . to_string( ) , r. port( ) ) ) . collect( ) ;
358+ kdc_hosts. extend( srv_records_to_kdc_urls( "tcp" , & records) ) ;
351359 }
352360
353361 if let Ok ( records) = execute_future( resolver. srv_lookup( format!( "_kerberos._udp.{domain}" ) ) ) {
354- for record in records {
355- let port = record. port( ) ;
356- let target_name = record. target( ) . to_string( ) ;
357- let target_name = target_name. trim_end_matches( '.' ) . to_string( ) ;
358- let kdc_host = format!( "udp://{}:{}" , & target_name, port) ;
359- kdc_hosts. push( kdc_host) ;
360- }
362+ let records: Vec <( String , u16 ) > =
363+ records. into_iter( ) . map( |r| ( r. target( ) . to_string( ) , r. port( ) ) ) . collect( ) ;
364+ kdc_hosts. extend( srv_records_to_kdc_urls( "udp" , & records) ) ;
361365 }
362366 }
363367
@@ -397,6 +401,21 @@ where
397401 }
398402}
399403
404+ /// Builds `scheme://host:port` KDC URLs from resolved SRV `(target, port)` records.
405+ ///
406+ /// DNS returns absolute names with a trailing dot (`dc.example.com.`); it is stripped so the
407+ /// result parses as a URL host.
408+ ///
409+ /// Limitation: SRV priority/weight (RFC 2782) are not applied here — candidates keep the order the
410+ /// platform resolver returned (Windows `DnsQuery_W` already sorts by priority/weight; hickory and
411+ /// `async_dnssd` do not). Proper priority ordering + weighted selection is a follow-up.
412+ fn srv_records_to_kdc_urls ( scheme : & str , records : & [ ( String , u16 ) ] ) -> Vec < String > {
413+ records
414+ . iter ( )
415+ . map ( |( target, port) | format ! ( "{scheme}://{}:{port}" , target. trim_end_matches( '.' ) ) )
416+ . collect ( )
417+ }
418+
400419#[ allow( unused_variables) ]
401420#[ instrument( level = "debug" , ret) ]
402421pub ( crate ) fn detect_kdc_hosts_from_dns ( domain : & str ) -> Vec < String > {
@@ -412,3 +431,34 @@ pub(crate) fn detect_kdc_hosts_from_dns(domain: &str) -> Vec<String> {
412431 }
413432 }
414433}
434+
435+ #[ cfg( test) ]
436+ mod tests {
437+ use super :: srv_records_to_kdc_urls;
438+
439+ #[ test]
440+ fn builds_scheme_host_port_and_preserves_order ( ) {
441+ let records = vec ! [ ( "dc1.example.com" . to_owned( ) , 88 ) , ( "dc2.example.com" . to_owned( ) , 8888 ) ] ;
442+ assert_eq ! (
443+ srv_records_to_kdc_urls( "tcp" , & records) ,
444+ vec![
445+ "tcp://dc1.example.com:88" . to_owned( ) ,
446+ "tcp://dc2.example.com:8888" . to_owned( )
447+ ]
448+ ) ;
449+ }
450+
451+ #[ test]
452+ fn strips_trailing_dot_from_absolute_names ( ) {
453+ let records = vec ! [ ( "dc.example.com." . to_owned( ) , 88 ) ] ;
454+ assert_eq ! (
455+ srv_records_to_kdc_urls( "udp" , & records) ,
456+ vec![ "udp://dc.example.com:88" . to_owned( ) ]
457+ ) ;
458+ }
459+
460+ #[ test]
461+ fn empty_input_yields_empty ( ) {
462+ assert ! ( srv_records_to_kdc_urls( "tcp" , & [ ] ) . is_empty( ) ) ;
463+ }
464+ }
0 commit comments