@@ -329,6 +329,47 @@ impl ServiceInfo {
329329 self . tls_domains . iter ( ) . map ( |d| d. domain . as_str ( ) ) . collect ( )
330330 }
331331
332+ /// Returns all domain names that require DNS records: TLS service domains
333+ /// plus UDP tracker domains (when a domain is explicitly configured).
334+ ///
335+ /// This is the correct method to use for the `provision` output `domains` array
336+ /// and the DNS setup reminder, because both need to list every hostname the
337+ /// operator must point at the server IP — including UDP trackers that have a
338+ /// domain name set.
339+ ///
340+ /// `tls_domain_names()` is kept for the HTTPS-specific hint that only lists
341+ /// TLS-enabled services.
342+ #[ must_use]
343+ pub fn all_domain_names ( & self ) -> Vec < & str > {
344+ let mut domains: Vec < & str > = self . tls_domain_names ( ) ;
345+ for url in & self . udp_trackers {
346+ // UDP tracker URLs are formatted as "udp://<host>:<port>/announce".
347+ // Extract the host part and include it only when it is a domain name
348+ // (i.e., it was originally set via UdpTrackerConfig::domain()).
349+ if let Some ( host) = Self :: extract_host_from_udp_url ( url) {
350+ // Only include if it looks like a domain name (not a raw IP address)
351+ if host. parse :: < std:: net:: IpAddr > ( ) . is_err ( ) {
352+ domains. push ( host) ;
353+ }
354+ }
355+ }
356+ domains
357+ }
358+
359+ /// Extracts the host portion from a UDP tracker announce URL.
360+ ///
361+ /// Expected format: `udp://<host>:<port>/announce`
362+ fn extract_host_from_udp_url ( url : & str ) -> Option < & str > {
363+ // Strip scheme
364+ let without_scheme = url. strip_prefix ( "udp://" ) ?;
365+ // Strip path suffix
366+ let host_and_port = without_scheme
367+ . strip_suffix ( "/announce" )
368+ . unwrap_or ( without_scheme) ;
369+ // Strip port
370+ host_and_port. rsplit_once ( ':' ) . map ( |( host, _port) | host)
371+ }
372+
332373 /// Returns all internal ports that are not exposed due to TLS
333374 #[ must_use]
334375 pub fn unexposed_ports ( & self ) -> Vec < u16 > {
@@ -519,4 +560,60 @@ mod tests {
519560 assert_eq ! ( services. localhost_http_trackers. len( ) , 1 ) ;
520561 assert_eq ! ( services. localhost_http_trackers[ 0 ] . port, 7070 ) ;
521562 }
563+
564+ #[ test]
565+ fn it_should_return_all_domain_names_including_udp ( ) {
566+ let services = ServiceInfo :: new (
567+ vec ! [
568+ "udp://udp1.tracker.local:6868/announce" . to_string( ) ,
569+ "udp://udp2.tracker.local:6969/announce" . to_string( ) ,
570+ ] ,
571+ vec ! [ "https://http1.tracker.local/announce" . to_string( ) ] ,
572+ vec ! [ ] ,
573+ vec ! [ ] ,
574+ "https://api.tracker.local/api" . to_string ( ) ,
575+ true ,
576+ false ,
577+ "http://10.0.0.1:1313/health_check" . to_string ( ) , // DevSkim: ignore DS137138
578+ false ,
579+ false ,
580+ vec ! [ TlsDomainInfo {
581+ domain: "http1.tracker.local" . to_string( ) ,
582+ internal_port: 7070 ,
583+ } ] ,
584+ ) ;
585+
586+ let domains = services. all_domain_names ( ) ;
587+ assert_eq ! ( domains. len( ) , 3 ) ;
588+ assert ! ( domains. contains( & "http1.tracker.local" ) ) ;
589+ assert ! ( domains. contains( & "udp1.tracker.local" ) ) ;
590+ assert ! ( domains. contains( & "udp2.tracker.local" ) ) ;
591+ }
592+
593+ #[ test]
594+ fn it_should_exclude_udp_trackers_without_domain_from_all_domain_names ( ) {
595+ let services = ServiceInfo :: new (
596+ // IP-only UDP trackers — no domain name
597+ vec ! [
598+ "udp://10.0.0.1:6868/announce" . to_string( ) ,
599+ "udp://10.0.0.2:6969/announce" . to_string( ) ,
600+ ] ,
601+ vec ! [ ] ,
602+ vec ! [ ] ,
603+ vec ! [ ] ,
604+ "http://10.0.0.1:1212/api" . to_string ( ) , // DevSkim: ignore DS137138
605+ false ,
606+ false ,
607+ "http://10.0.0.1:1313/health_check" . to_string ( ) , // DevSkim: ignore DS137138
608+ false ,
609+ false ,
610+ vec ! [ ] ,
611+ ) ;
612+
613+ let domains = services. all_domain_names ( ) ;
614+ assert ! (
615+ domains. is_empty( ) ,
616+ "IP-only UDP trackers must not appear in all_domain_names()"
617+ ) ;
618+ }
522619}
0 commit comments