Skip to content

Commit bca1a2a

Browse files
committed
fix: [#412] include UDP tracker domains in provision output and DNS reminder
1 parent a174f4a commit bca1a2a

3 files changed

Lines changed: 126 additions & 4 deletions

File tree

src/application/command_handlers/show/info/tracker.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/presentation/cli/views/commands/provision/view_data/dns_reminder.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,8 @@ impl DnsReminderView {
158158
/// ```
159159
#[must_use]
160160
pub fn extract_all_domains(services: &ServiceInfo) -> Vec<String> {
161-
// Currently, ServiceInfo only tracks TLS domains
162-
// This returns all domain names from tls_domains
163161
services
164-
.tls_domain_names()
162+
.all_domain_names()
165163
.iter()
166164
.map(|s| (*s).to_string())
167165
.collect()
@@ -238,6 +236,33 @@ mod tests {
238236
assert!(domains.contains(&"health.tracker.local".to_string()));
239237
}
240238

239+
#[test]
240+
fn it_should_include_udp_tracker_domains_in_extract_all_domains() {
241+
let services = ServiceInfo::new(
242+
vec![
243+
"udp://udp1.tracker.local:6868/announce".to_string(),
244+
"udp://udp2.tracker.local:6969/announce".to_string(),
245+
],
246+
vec!["https://http.tracker.local/announce".to_string()],
247+
vec![],
248+
vec![],
249+
"https://api.tracker.local/api".to_string(),
250+
true,
251+
false,
252+
"http://10.0.0.1:1313/health_check".to_string(), // DevSkim: ignore DS137138
253+
false,
254+
false,
255+
vec![TlsDomainInfo::new("http.tracker.local".to_string(), 7070)],
256+
);
257+
258+
let domains = DnsReminderView::extract_all_domains(&services);
259+
260+
assert_eq!(domains.len(), 3);
261+
assert!(domains.contains(&"http.tracker.local".to_string()));
262+
assert!(domains.contains(&"udp1.tracker.local".to_string()));
263+
assert!(domains.contains(&"udp2.tracker.local".to_string()));
264+
}
265+
241266
#[test]
242267
fn it_should_return_empty_vec_when_no_domains_configured() {
243268
let services = ServiceInfo::new(

src/presentation/cli/views/commands/provision/view_data/provision_details.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ impl From<&Environment<Provisioned>> for ProvisionDetailsData {
8080
let grafana_config = environment.grafana_config();
8181
let services = ServiceInfo::from_tracker_config(tracker_config, ip, grafana_config);
8282
services
83-
.tls_domain_names()
83+
.all_domain_names()
8484
.iter()
8585
.map(|s| (*s).to_string())
8686
.collect()

0 commit comments

Comments
 (0)