Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions schemas/environment-config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
Running `target/debug/torrust-tracker-deployer create schema`
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "EnvironmentCreationConfig",
Expand Down Expand Up @@ -460,6 +462,13 @@
"properties": {
"bind_address": {
"type": "string"
},
"domain": {
"description": "Domain name for the UDP tracker (optional)\n\nWhen present, this domain can be used in announce URLs instead of the IP.\nExample: `udp://tracker.example.com:6969/announce`\n\nNote: Unlike HTTP trackers, UDP does not support TLS, so there is no\n`use_tls_proxy` field for UDP trackers.",
"type": [
"string",
"null"
]
}
},
"required": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ impl EnvironmentCreationConfig {
},
udp_trackers: vec![super::tracker::UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![super::tracker::HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down Expand Up @@ -1407,6 +1408,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down Expand Up @@ -1482,6 +1484,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down Expand Up @@ -1573,6 +1576,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ impl Default for TrackerSection {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down Expand Up @@ -161,6 +162,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down Expand Up @@ -205,9 +207,11 @@ mod tests {
udp_trackers: vec![
UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
},
UdpTrackerSection {
bind_address: "0.0.0.0:6970".to_string(),
domain: None,
},
],
http_trackers: vec![
Expand Down Expand Up @@ -248,6 +252,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "invalid".to_string(),
domain: None,
}],
http_trackers: vec![],
http_api: HttpApiSection {
Expand Down Expand Up @@ -279,6 +284,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down Expand Up @@ -378,6 +384,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
domain: None,
}],
http_trackers: vec![HttpTrackerSection {
bind_address: "0.0.0.0:7070".to_string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ use serde::{Deserialize, Serialize};

use crate::application::command_handlers::create::config::errors::CreateConfigError;
use crate::domain::tracker::UdpTrackerConfig;
use crate::shared::DomainName;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct UdpTrackerSection {
pub bind_address: String,

/// Domain name for the UDP tracker (optional)
///
/// When present, this domain can be used in announce URLs instead of the IP.
/// Example: `udp://tracker.example.com:6969/announce`
///
/// Note: Unlike HTTP trackers, UDP does not support TLS, so there is no
/// `use_tls_proxy` field for UDP trackers.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
}

impl UdpTrackerSection {
Expand All @@ -18,6 +29,7 @@ impl UdpTrackerSection {
///
/// Returns `CreateConfigError::InvalidBindAddress` if the bind address cannot be parsed as a valid IP:PORT combination.
/// Returns `CreateConfigError::DynamicPortNotSupported` if port 0 (dynamic port assignment) is specified.
/// Returns `CreateConfigError::InvalidDomain` if the domain is invalid.
pub fn to_udp_tracker_config(&self) -> Result<UdpTrackerConfig, CreateConfigError> {
// Validate that the bind address can be parsed as SocketAddr
let bind_address = self.bind_address.parse::<SocketAddr>().map_err(|e| {
Expand All @@ -34,8 +46,23 @@ impl UdpTrackerSection {
});
}

// Domain type now uses SocketAddr (Step 0.7 completed)
Ok(UdpTrackerConfig { bind_address })
// Convert domain to domain type with validation (if present)
let domain = match &self.domain {
Some(domain_str) => {
let domain =
DomainName::new(domain_str).map_err(|e| CreateConfigError::InvalidDomain {
domain: domain_str.clone(),
reason: e.to_string(),
})?;
Some(domain)
}
None => None,
};

Ok(UdpTrackerConfig {
bind_address,
domain,
})
}
}

Expand All @@ -47,6 +74,7 @@ mod tests {
fn it_should_convert_valid_bind_address_to_udp_tracker_config() {
let section = UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
};

let result = section.to_udp_tracker_config();
Expand All @@ -57,12 +85,52 @@ mod tests {
config.bind_address,
"0.0.0.0:6969".parse::<SocketAddr>().unwrap()
);
assert!(config.domain.is_none());
}

#[test]
fn it_should_convert_with_valid_domain() {
let section = UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: Some("udp.tracker.local".to_string()),
};

let result = section.to_udp_tracker_config();
assert!(result.is_ok());

let config = result.unwrap();
assert_eq!(
config.bind_address,
"0.0.0.0:6969".parse::<SocketAddr>().unwrap()
);
assert_eq!(
config.domain.as_ref().map(DomainName::as_str),
Some("udp.tracker.local")
);
}

#[test]
fn it_should_fail_for_invalid_domain() {
let section = UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: Some(String::new()), // Empty domain is invalid
};

let result = section.to_udp_tracker_config();
assert!(result.is_err());

if let Err(CreateConfigError::InvalidDomain { domain, .. }) = result {
assert_eq!(domain, "");
} else {
panic!("Expected InvalidDomain error");
}
}

#[test]
fn it_should_fail_for_invalid_bind_address() {
let section = UdpTrackerSection {
bind_address: "invalid".to_string(),
domain: None,
};

let result = section.to_udp_tracker_config();
Expand All @@ -79,6 +147,7 @@ mod tests {
fn it_should_reject_port_zero() {
let section = UdpTrackerSection {
bind_address: "0.0.0.0:0".to_string(),
domain: None,
};

let result = section.to_udp_tracker_config();
Expand All @@ -92,20 +161,46 @@ mod tests {
}

#[test]
fn it_should_be_serializable() {
fn it_should_be_serializable_without_domain() {
let section = UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: None,
};

let json = serde_json::to_string(&section).unwrap();
assert!(json.contains("bind_address"));
assert!(json.contains("0.0.0.0:6969"));
// domain should not be present when None (skip_serializing_if)
assert!(!json.contains("domain"));
}

#[test]
fn it_should_be_deserializable() {
fn it_should_be_serializable_with_domain() {
let section = UdpTrackerSection {
bind_address: "0.0.0.0:6969".to_string(),
domain: Some("udp.tracker.local".to_string()),
};

let json = serde_json::to_string(&section).unwrap();
assert!(json.contains("bind_address"));
assert!(json.contains("0.0.0.0:6969"));
assert!(json.contains("domain"));
assert!(json.contains("udp.tracker.local"));
}

#[test]
fn it_should_be_deserializable_without_domain() {
let json = r#"{"bind_address":"0.0.0.0:6969"}"#;
let section: UdpTrackerSection = serde_json::from_str(json).unwrap();
assert_eq!(section.bind_address, "0.0.0.0:6969");
assert!(section.domain.is_none());
}

#[test]
fn it_should_be_deserializable_with_domain() {
let json = r#"{"bind_address":"0.0.0.0:6969","domain":"udp.tracker.local"}"#;
let section: UdpTrackerSection = serde_json::from_str(json).unwrap();
assert_eq!(section.bind_address, "0.0.0.0:6969");
assert_eq!(section.domain, Some("udp.tracker.local".to_string()));
}
}
9 changes: 8 additions & 1 deletion src/application/command_handlers/show/info/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ impl ServiceInfo {
/// * `instance_ip` - The IP address of the deployed instance
/// * `grafana_config` - Optional Grafana configuration (for TLS domain info)
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn from_tracker_config(
tracker_config: &TrackerConfig,
instance_ip: IpAddr,
Expand All @@ -132,7 +133,13 @@ impl ServiceInfo {
let udp_trackers = tracker_config
.udp_trackers
.iter()
.map(|udp| format!("udp://{}:{}/announce", instance_ip, udp.bind_address.port()))
.map(|udp| {
let host = udp
.domain
.as_ref()
.map_or_else(|| instance_ip.to_string(), |d| d.as_str().to_string());
format!("udp://{}:{}/announce", host, udp.bind_address.port())
})
.collect();

// Separate HTTP trackers by TLS configuration and localhost status
Expand Down
10 changes: 8 additions & 2 deletions src/domain/tracker/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub fn is_localhost(addr: &SocketAddr) -> bool {
/// private: false,
/// },
/// udp_trackers: vec![
/// UdpTrackerConfig { bind_address: "0.0.0.0:6969".parse().unwrap() },
/// UdpTrackerConfig { bind_address: "0.0.0.0:6969".parse().unwrap(), domain: None },
/// ],
/// http_trackers: vec![
/// HttpTrackerConfig { bind_address: "0.0.0.0:7070".parse().unwrap(), domain: None, use_tls_proxy: false },
Expand Down Expand Up @@ -274,7 +274,7 @@ impl TrackerConfig {
/// private: false,
/// },
/// udp_trackers: vec![
/// UdpTrackerConfig { bind_address: "0.0.0.0:6969".parse().unwrap() },
/// UdpTrackerConfig { bind_address: "0.0.0.0:6969".parse().unwrap(), domain: None },
/// ],
/// http_trackers: vec![
/// HttpTrackerConfig { bind_address: "0.0.0.0:7070".parse().unwrap(), domain: None, use_tls_proxy: false },
Expand Down Expand Up @@ -537,6 +537,7 @@ impl Default for TrackerConfig {
},
udp_trackers: vec![UdpTrackerConfig {
bind_address: "0.0.0.0:6969".parse().expect("valid address"),
domain: None,
}],
http_trackers: vec![HttpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().expect("valid address"),
Expand Down Expand Up @@ -629,6 +630,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerConfig {
bind_address: "0.0.0.0:6868".parse().unwrap(),
domain: None,
}],
http_trackers: vec![HttpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().unwrap(),
Expand Down Expand Up @@ -730,6 +732,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerConfig {
bind_address: "0.0.0.0:6969".parse().unwrap(),
domain: None,
}],
http_trackers: vec![HttpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().unwrap(),
Expand Down Expand Up @@ -764,9 +767,11 @@ mod tests {
udp_trackers: vec![
UdpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().unwrap(),
domain: None,
},
UdpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().unwrap(),
domain: None,
},
],
http_trackers: vec![],
Expand Down Expand Up @@ -960,6 +965,7 @@ mod tests {
},
udp_trackers: vec![UdpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().unwrap(),
domain: None,
}],
http_trackers: vec![HttpTrackerConfig {
bind_address: "0.0.0.0:7070".parse().unwrap(),
Expand Down
Loading
Loading