@@ -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,33 @@ 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( ) . cast:: <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+ // UNTESTED: `DNS_RECORDA` and `DNS_RECORDW` share one layout (`PSTR` and `PWSTR` are both pointers),
38+ // so the list `DnsQuery_W` types as `DNS_RECORDA` is reinterpreted as `DNS_RECORDW` once and the
39+ // record types tell the truth for the rest of the walk.
40+ // Nothing here runs under test, so when bumping the `windows` crate compare both struct definitions
41+ // in `Win32::NetworkManagement::Dns` field by field.
42+ let mut p_record = p_query_results. cast:: <DNS_RECORDW >( ) ;
43+ while !p_record. is_null( ) {
44+ // SAFETY: `p_record` is non-null and points to a valid record of the list `DnsQuery_W`
45+ // returned.
46+ let record = unsafe { * p_record } ;
47+
48+ if record. wType == DNS_TYPE_SRV . 0 {
49+ // SAFETY: `wType == DNS_TYPE_SRV` guarantees the `Srv` union member is active.
50+ let srv = unsafe { record. Data . Srv } ;
51+ // SAFETY: `pNameTarget` points to a valid, null-terminated UTF-16 string.
52+ if let Ok ( name_target) = unsafe { srv. pNameTarget. to_string( ) } {
53+ records. push( ( name_target, srv. wPort) ) ;
54+ }
55+ }
56+
57+ p_record = record. pNext;
4458 }
4559 }
4660 Err ( error) => error!( %error, "DnsQuery_W failed" ) ,
4761 }
4862
49- // SAFETY: `p_query_results` is not null.
63+ // SAFETY: `DnsFree` tolerates a null pointer .
5064 unsafe {
5165 DnsFree ( Some ( p_query_results. cast:: <c_void>( ) ) , DnsFreeRecordList ) ;
5266 }
@@ -136,14 +150,14 @@ cfg_if::cfg_if! {
136150 let krb_tcp_srv = dns_query_srv_records( krb_tcp_name) ;
137151
138152 if !krb_tcp_srv. is_empty( ) {
139- return krb_tcp_srv . iter ( ) . map ( |x| format! ( "tcp://{x}:88" ) ) . collect ( )
153+ return srv_records_to_kdc_urls ( "tcp" , & krb_tcp_srv )
140154 }
141155
142156 let krb_udp_name = & format!( "_kerberos._udp.{domain}" ) ;
143157 let krb_udp_srv = dns_query_srv_records( krb_udp_name) ;
144158
145159 if !krb_udp_srv. is_empty( ) {
146- return krb_udp_srv . iter ( ) . map ( |x| format! ( "udp://{x}:88" ) ) . collect ( )
160+ return srv_records_to_kdc_urls ( "udp" , & krb_udp_srv )
147161 }
148162
149163 Vec :: new( )
@@ -335,14 +349,16 @@ cfg_if::cfg_if! {
335349 let krb_tcp_srv = dns_query_srv_records( krb_tcp_name) ;
336350
337351 if !krb_tcp_srv. is_empty( ) {
338- return krb_tcp_srv. iter( ) . map( |x| format!( "tcp://{}:{}" , x. target, x. port) ) . collect( )
352+ let records: Vec <( String , u16 ) > = krb_tcp_srv. into_iter( ) . map( |x| ( x. target, x. port) ) . collect( ) ;
353+ return srv_records_to_kdc_urls( "tcp" , & records)
339354 }
340355
341356 let krb_udp_name = & format!( "_kerberos._udp.{domain}" ) ;
342357 let krb_udp_srv = dns_query_srv_records( krb_udp_name) ;
343358
344359 if !krb_udp_srv. is_empty( ) {
345- return krb_udp_srv. iter( ) . map( |x| format!( "udp://{}:{}" , x. target, x. port) ) . collect( )
360+ let records: Vec <( String , u16 ) > = krb_udp_srv. into_iter( ) . map( |x| ( x. target, x. port) ) . collect( ) ;
361+ return srv_records_to_kdc_urls( "udp" , & records)
346362 }
347363
348364 Vec :: new( )
@@ -437,23 +453,15 @@ cfg_if::cfg_if! {
437453
438454 if let Some ( resolver) = get_dns_resolver( domain) {
439455 if let Ok ( records) = execute_future( resolver. srv_lookup( format!( "_kerberos._tcp.{domain}" ) ) ) {
440- for record in records {
441- let port = record. port( ) ;
442- let target_name = record. target( ) . to_string( ) ;
443- let target_name = target_name. trim_end_matches( '.' ) . to_string( ) ;
444- let kdc_host = format!( "tcp://{}:{}" , target_name, port) ;
445- kdc_hosts. push( kdc_host) ;
446- }
456+ let records: Vec <( String , u16 ) > =
457+ records. into_iter( ) . map( |r| ( r. target( ) . to_string( ) , r. port( ) ) ) . collect( ) ;
458+ kdc_hosts. extend( srv_records_to_kdc_urls( "tcp" , & records) ) ;
447459 }
448460
449461 if let Ok ( records) = execute_future( resolver. srv_lookup( format!( "_kerberos._udp.{domain}" ) ) ) {
450- for record in records {
451- let port = record. port( ) ;
452- let target_name = record. target( ) . to_string( ) ;
453- let target_name = target_name. trim_end_matches( '.' ) . to_string( ) ;
454- let kdc_host = format!( "udp://{}:{}" , target_name, port) ;
455- kdc_hosts. push( kdc_host) ;
456- }
462+ let records: Vec <( String , u16 ) > =
463+ records. into_iter( ) . map( |r| ( r. target( ) . to_string( ) , r. port( ) ) ) . collect( ) ;
464+ kdc_hosts. extend( srv_records_to_kdc_urls( "udp" , & records) ) ;
457465 }
458466 }
459467
@@ -493,6 +501,23 @@ where
493501 }
494502}
495503
504+ /// Builds `scheme://host:port` KDC URLs from resolved SRV `(target, port)` records.
505+ ///
506+ /// DNS returns absolute names with a trailing dot (`dc.example.com.`); it is stripped so the result parses as a URL host.
507+ ///
508+ /// Limitation: the SRV priority and weight of [RFC 2782] are not applied, so candidates keep the order the platform
509+ /// resolver returned.
510+ /// Windows `DnsQuery_W` already sorts by priority and weight; hickory and `async_dnssd` do not.
511+ /// Tracked in <https://github.com/Devolutions/sspi-rs/issues/787>.
512+ ///
513+ /// [RFC 2782]: https://www.rfc-editor.org/rfc/rfc2782
514+ fn srv_records_to_kdc_urls ( scheme : & str , records : & [ ( String , u16 ) ] ) -> Vec < String > {
515+ records
516+ . iter ( )
517+ . map ( |( target, port) | format ! ( "{scheme}://{}:{port}" , target. trim_end_matches( '.' ) ) )
518+ . collect ( )
519+ }
520+
496521#[ allow( unused_variables) ]
497522#[ instrument( level = "debug" , ret) ]
498523pub ( crate ) fn detect_kdc_hosts_from_dns ( domain : & str ) -> Vec < String > {
@@ -607,3 +632,34 @@ mod apple_srv_tests {
607632 ) ) ;
608633 }
609634}
635+
636+ #[ cfg( test) ]
637+ mod tests {
638+ use super :: srv_records_to_kdc_urls;
639+
640+ #[ test]
641+ fn builds_scheme_host_port_and_preserves_order ( ) {
642+ let records = vec ! [ ( "dc1.example.com" . to_owned( ) , 88 ) , ( "dc2.example.com" . to_owned( ) , 8888 ) ] ;
643+ assert_eq ! (
644+ srv_records_to_kdc_urls( "tcp" , & records) ,
645+ vec![
646+ "tcp://dc1.example.com:88" . to_owned( ) ,
647+ "tcp://dc2.example.com:8888" . to_owned( )
648+ ]
649+ ) ;
650+ }
651+
652+ #[ test]
653+ fn strips_trailing_dot_from_absolute_names ( ) {
654+ let records = vec ! [ ( "dc.example.com." . to_owned( ) , 88 ) ] ;
655+ assert_eq ! (
656+ srv_records_to_kdc_urls( "udp" , & records) ,
657+ vec![ "udp://dc.example.com:88" . to_owned( ) ]
658+ ) ;
659+ }
660+
661+ #[ test]
662+ fn empty_input_yields_empty ( ) {
663+ assert ! ( srv_records_to_kdc_urls( "tcp" , & [ ] ) . is_empty( ) ) ;
664+ }
665+ }
0 commit comments