1use std::collections::HashMap;
7use std::fmt;
8use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
9
10use serde::{Deserialize, Serialize};
11
12use super::{BindingAddress, Protocol};
13use crate::domain::topology::{
14 EnabledServices, Network, NetworkDerivation, PortBinding, PortDerivation, Service,
15};
16use crate::shared::docker_image::DockerImage;
17use crate::shared::DomainName;
18
19pub const TRACKER_DOCKER_IMAGE_REPOSITORY: &str = "torrust/tracker";
21
22pub const TRACKER_DOCKER_IMAGE_TAG: &str = "develop";
24
25mod core;
26mod health_check_api;
27mod http;
28mod http_api;
29mod udp;
30
31pub use core::{
32 DatabaseConfig, MysqlConfig, MysqlConfigError, SqliteConfig, SqliteConfigError,
33 TrackerCoreConfig,
34};
35pub use health_check_api::{HealthCheckApiConfig, HealthCheckApiConfigError};
36pub use http::{HttpTrackerConfig, HttpTrackerConfigError};
37pub use http_api::{HttpApiConfig, HttpApiConfigError};
38pub use udp::{UdpTrackerConfig, UdpTrackerConfigError};
39
40#[must_use]
55pub fn is_localhost(addr: &SocketAddr) -> bool {
56 match addr.ip() {
57 IpAddr::V4(ipv4) => ipv4 == Ipv4Addr::LOCALHOST,
58 IpAddr::V6(ipv6) => ipv6 == Ipv6Addr::LOCALHOST,
59 }
60}
61
62#[derive(Debug, Clone, Serialize, PartialEq)]
96pub struct TrackerConfig {
97 core: TrackerCoreConfig,
99
100 udp_trackers: Vec<UdpTrackerConfig>,
102
103 http_trackers: Vec<HttpTrackerConfig>,
105
106 http_api: HttpApiConfig,
108
109 health_check_api: HealthCheckApiConfig,
111}
112
113#[derive(Debug, Clone, PartialEq)]
115pub enum TrackerConfigError {
116 DuplicateSocketAddress {
118 address: SocketAddr,
120 protocol: Protocol,
122 services: Vec<String>,
124 },
125}
126
127impl fmt::Display for TrackerConfigError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Self::DuplicateSocketAddress {
131 address,
132 protocol,
133 services,
134 } => {
135 let services_list = services
136 .iter()
137 .map(|s| format!("'{s}'"))
138 .collect::<Vec<_>>()
139 .join(", ");
140 write!(
141 f,
142 "Socket address conflict: {services_list} cannot bind to {address} ({protocol})\n\
143 Tip: Assign different port numbers to each service"
144 )
145 }
146 }
147 }
148}
149
150impl std::error::Error for TrackerConfigError {}
151
152impl TrackerConfigError {
153 #[must_use]
158 pub fn help(&self) -> String {
159 match self {
160 Self::DuplicateSocketAddress {
161 address,
162 protocol,
163 services,
164 } => {
165 use std::fmt::Write;
166
167 let mut help =
168 String::from("Socket Address Conflict - Detailed Troubleshooting:\n\n");
169
170 help.push_str("Conflicting services:\n");
171 for service in services {
172 let _ = writeln!(help, " - {service}: {address} ({protocol})");
173 }
174 help.push('\n');
175
176 help.push_str("Why this fails:\n");
177 let _ = write!(
178 help,
179 "Two services using the same protocol ({protocol}) cannot bind to the same\n\
180 IP address and port number. The second service will fail with\n\
181 \"Address already in use\" error.\n\n"
182 );
183
184 help.push_str("How to fix:\n");
185 help.push_str(
186 "1. Assign different port numbers to each service\n\
187 2. Or configure only one service to use this address\n\n",
188 );
189
190 help.push_str("Note:\n");
191 help.push_str(
192 "Services using different protocols (UDP vs TCP) CAN share the same port.\n\
193 See: docs/external-issues/tracker/udp-tcp-port-sharing-allowed.md\n",
194 );
195
196 help
197 }
198 }
199 }
200}
201
202impl TrackerConfig {
203 pub fn new(
248 core: TrackerCoreConfig,
249 udp_trackers: Vec<UdpTrackerConfig>,
250 http_trackers: Vec<HttpTrackerConfig>,
251 http_api: HttpApiConfig,
252 health_check_api: HealthCheckApiConfig,
253 ) -> Result<Self, TrackerConfigError> {
254 let config = Self {
255 core,
256 udp_trackers,
257 http_trackers,
258 http_api,
259 health_check_api,
260 };
261
262 config.check_socket_address_conflicts()?;
265
266 Ok(config)
267 }
268
269 #[must_use]
271 pub fn core(&self) -> &TrackerCoreConfig {
272 &self.core
273 }
274
275 #[must_use]
277 pub fn udp_trackers(&self) -> &[UdpTrackerConfig] {
278 &self.udp_trackers
279 }
280
281 #[must_use]
283 pub fn http_trackers(&self) -> &[HttpTrackerConfig] {
284 &self.http_trackers
285 }
286
287 #[must_use]
289 pub fn http_api(&self) -> &HttpApiConfig {
290 &self.http_api
291 }
292
293 #[must_use]
295 pub fn health_check_api(&self) -> &HealthCheckApiConfig {
296 &self.health_check_api
297 }
298
299 #[must_use]
336 pub fn uses_mysql(&self) -> bool {
337 matches!(self.core.database(), DatabaseConfig::Mysql(_))
338 }
339
340 #[must_use]
353 pub fn docker_image() -> DockerImage {
354 DockerImage::new(TRACKER_DOCKER_IMAGE_REPOSITORY, TRACKER_DOCKER_IMAGE_TAG)
355 }
356
357 fn check_socket_address_conflicts(&self) -> Result<(), TrackerConfigError> {
362 let bindings = self.collect_bindings();
363 Self::check_for_conflicts(bindings)
364 }
365
366 fn check_for_conflicts(
376 bindings: HashMap<BindingAddress, Vec<String>>,
377 ) -> Result<(), TrackerConfigError> {
378 for (binding, services) in bindings {
379 if services.len() > 1 {
380 return Err(TrackerConfigError::DuplicateSocketAddress {
381 address: *binding.socket(),
382 protocol: binding.protocol(),
383 services,
384 });
385 }
386 }
387
388 Ok(())
389 }
390
391 fn collect_bindings(&self) -> HashMap<BindingAddress, Vec<String>> {
397 let mut bindings: HashMap<BindingAddress, Vec<String>> = HashMap::new();
398
399 Self::register_trackers(
401 &mut bindings,
402 &self.udp_trackers,
403 Protocol::Udp,
404 "UDP Tracker",
405 );
406
407 Self::register_trackers(
409 &mut bindings,
410 &self.http_trackers,
411 Protocol::Tcp,
412 "HTTP Tracker",
413 );
414
415 Self::register_binding(
417 &mut bindings,
418 self.http_api.bind_address(),
419 Protocol::Tcp,
420 "HTTP API",
421 );
422
423 Self::register_binding(
425 &mut bindings,
426 self.health_check_api.bind_address(),
427 Protocol::Tcp,
428 "Health Check API",
429 );
430
431 bindings
432 }
433
434 fn register_trackers<T>(
438 bindings: &mut HashMap<BindingAddress, Vec<String>>,
439 trackers: &[T],
440 protocol: Protocol,
441 service_name: &str,
442 ) where
443 T: HasBindAddress,
444 {
445 for (i, tracker) in trackers.iter().enumerate() {
446 let service_label = format!("{service_name} #{}", i + 1);
447 Self::register_binding(bindings, tracker.bind_address(), protocol, &service_label);
448 }
449 }
450
451 fn register_binding(
455 bindings: &mut HashMap<BindingAddress, Vec<String>>,
456 address: SocketAddr,
457 protocol: Protocol,
458 service_name: &str,
459 ) {
460 let binding = BindingAddress::new(address, protocol);
461 bindings
462 .entry(binding)
463 .or_default()
464 .push(service_name.to_string());
465 }
466
467 #[must_use]
469 pub fn http_api_tls_domain(&self) -> Option<&str> {
470 self.http_api.tls_domain().map(DomainName::as_str)
471 }
472
473 #[must_use]
475 pub fn http_api_port(&self) -> u16 {
476 self.http_api.bind_address().port()
477 }
478
479 #[must_use]
481 pub fn health_check_api_tls_domain(&self) -> Option<&str> {
482 self.health_check_api.tls_domain()
483 }
484
485 #[must_use]
487 pub fn health_check_api_port(&self) -> u16 {
488 self.health_check_api.bind_address().port()
489 }
490
491 #[must_use]
496 pub fn http_trackers_with_tls(&self) -> Vec<(&str, u16)> {
497 self.http_trackers
498 .iter()
499 .filter(|tracker| tracker.use_tls_proxy())
500 .filter_map(|tracker| {
501 tracker
502 .domain()
503 .map(|domain| (domain.as_str(), tracker.bind_address().port()))
504 })
505 .collect()
506 }
507
508 #[must_use]
513 pub fn any_http_tracker_uses_tls_proxy(&self) -> bool {
514 self.http_trackers
515 .iter()
516 .any(http::HttpTrackerConfig::use_tls_proxy)
517 }
518
519 #[must_use]
529 pub fn has_any_tls_configured(&self) -> bool {
530 self.http_api.use_tls_proxy()
531 || self
532 .http_trackers
533 .iter()
534 .any(http::HttpTrackerConfig::use_tls_proxy)
535 || self.health_check_api.use_tls_proxy()
536 }
537}
538
539impl PortDerivation for TrackerConfig {
540 fn derive_ports(&self) -> Vec<PortBinding> {
550 let mut ports = Vec::new();
551
552 for udp_tracker in &self.udp_trackers {
554 ports.push(PortBinding::udp(
555 udp_tracker.bind_address().port(),
556 "BitTorrent UDP announce",
557 ));
558 }
559
560 for http_tracker in &self.http_trackers {
563 if !http_tracker.use_tls_proxy() {
564 ports.push(PortBinding::tcp(
565 http_tracker.bind_address().port(),
566 "HTTP tracker announce",
567 ));
568 }
569 }
570
571 if !self.http_api.use_tls_proxy() {
574 ports.push(PortBinding::tcp(
575 self.http_api.bind_address().port(),
576 "HTTP API (stats/whitelist)",
577 ));
578 }
579
580 ports
581 }
582}
583
584impl NetworkDerivation for TrackerConfig {
585 fn derive_networks(&self, enabled_services: &EnabledServices) -> Vec<Network> {
592 let mut networks = Vec::new();
593
594 if enabled_services.has(Service::Prometheus) {
596 networks.push(Network::Metrics);
597 }
598
599 if enabled_services.has(Service::MySQL) {
601 networks.push(Network::Database);
602 }
603
604 if enabled_services.has(Service::Caddy) {
606 networks.push(Network::Proxy);
607 }
608
609 networks
610 }
611}
612
613trait HasBindAddress {
617 fn bind_address(&self) -> SocketAddr;
619}
620
621impl HasBindAddress for UdpTrackerConfig {
622 fn bind_address(&self) -> SocketAddr {
623 UdpTrackerConfig::bind_address(self)
624 }
625}
626
627impl HasBindAddress for HttpTrackerConfig {
628 fn bind_address(&self) -> SocketAddr {
629 HttpTrackerConfig::bind_address(self)
630 }
631}
632
633impl Default for TrackerConfig {
634 fn default() -> Self {
645 Self::new(
646 TrackerCoreConfig::new(
647 DatabaseConfig::Sqlite(
648 SqliteConfig::new("tracker.db").expect("default sqlite config is valid"),
649 ),
650 false,
651 ),
652 vec![
653 UdpTrackerConfig::new("0.0.0.0:6969".parse().expect("valid address"), None)
654 .expect("default UdpTrackerConfig values are always valid"),
655 ],
656 vec![HttpTrackerConfig::new(
657 "0.0.0.0:7070".parse().expect("valid address"),
658 None,
659 false,
660 )
661 .expect("default HttpTrackerConfig values are always valid")],
662 HttpApiConfig::new(
663 "0.0.0.0:1212".parse().expect("valid address"),
664 "MyAccessToken".to_string().into(),
665 None,
666 false,
667 )
668 .expect("default HttpApiConfig values are always valid"),
669 HealthCheckApiConfig::new(
670 "127.0.0.1:1313".parse().expect("valid address"),
671 None,
672 false,
673 )
674 .expect("default HealthCheckApiConfig values are always valid"),
675 )
676 .expect("default TrackerConfig values have no socket address conflicts")
677 }
678}
679
680pub(crate) fn serialize_socket_addr<S>(addr: &SocketAddr, serializer: S) -> Result<S::Ok, S::Error>
681where
682 S: serde::Serializer,
683{
684 serializer.serialize_str(&addr.to_string())
685}
686
687pub(crate) fn deserialize_socket_addr<'de, D>(deserializer: D) -> Result<SocketAddr, D::Error>
688where
689 D: serde::Deserializer<'de>,
690{
691 let s = String::deserialize(deserializer)?;
692 s.parse().map_err(serde::de::Error::custom)
693}
694
695#[derive(Deserialize)]
697struct TrackerConfigRaw {
698 core: TrackerCoreConfig,
699 udp_trackers: Vec<UdpTrackerConfig>,
700 http_trackers: Vec<HttpTrackerConfig>,
701 http_api: HttpApiConfig,
702 health_check_api: HealthCheckApiConfig,
703}
704
705impl<'de> Deserialize<'de> for TrackerConfig {
706 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
707 where
708 D: serde::Deserializer<'de>,
709 {
710 let raw = TrackerConfigRaw::deserialize(deserializer)?;
711 TrackerConfig::new(
712 raw.core,
713 raw.udp_trackers,
714 raw.http_trackers,
715 raw.http_api,
716 raw.health_check_api,
717 )
718 .map_err(serde::de::Error::custom)
719 }
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 fn test_udp_tracker_config(bind_address: &str) -> UdpTrackerConfig {
729 UdpTrackerConfig::new(bind_address.parse().expect("valid address"), None)
730 .expect("test values should be valid")
731 }
732
733 #[allow(dead_code)]
735 fn test_udp_tracker_config_with_domain(
736 bind_address: &str,
737 domain: DomainName,
738 ) -> UdpTrackerConfig {
739 UdpTrackerConfig::new(bind_address.parse().expect("valid address"), Some(domain))
740 .expect("test values should be valid")
741 }
742
743 fn test_http_tracker_config(bind_address: &str) -> HttpTrackerConfig {
745 HttpTrackerConfig::new(bind_address.parse().expect("valid address"), None, false)
746 .expect("test values should be valid")
747 }
748
749 #[allow(dead_code)]
751 fn test_http_tracker_config_with_tls(
752 bind_address: &str,
753 domain: Option<DomainName>,
754 use_tls_proxy: bool,
755 ) -> HttpTrackerConfig {
756 HttpTrackerConfig::new(
757 bind_address.parse().expect("valid address"),
758 domain,
759 use_tls_proxy,
760 )
761 .expect("test values should be valid")
762 }
763
764 fn test_health_check_api_config(bind_address: &str) -> HealthCheckApiConfig {
766 HealthCheckApiConfig::new(bind_address.parse().expect("valid address"), None, false)
767 .expect("test values should be valid")
768 }
769
770 #[allow(dead_code)]
772 fn test_health_check_api_config_with_tls(
773 bind_address: &str,
774 domain: Option<DomainName>,
775 use_tls_proxy: bool,
776 ) -> HealthCheckApiConfig {
777 HealthCheckApiConfig::new(
778 bind_address.parse().expect("valid address"),
779 domain,
780 use_tls_proxy,
781 )
782 .expect("test values should be valid")
783 }
784
785 fn test_http_api_config(bind_address: &str, admin_token: &str) -> HttpApiConfig {
788 HttpApiConfig::new(
789 bind_address.parse().expect("valid address"),
790 admin_token.to_string().into(),
791 None,
792 false,
793 )
794 .expect("test values should be valid")
795 }
796
797 fn test_http_api_config_with_tls(
799 bind_address: &str,
800 admin_token: &str,
801 domain: Option<DomainName>,
802 use_tls_proxy: bool,
803 ) -> HttpApiConfig {
804 HttpApiConfig::new(
805 bind_address.parse().expect("valid address"),
806 admin_token.to_string().into(),
807 domain,
808 use_tls_proxy,
809 )
810 .expect("test values should be valid")
811 }
812
813 fn test_tracker_config(
816 udp_trackers: Vec<UdpTrackerConfig>,
817 http_trackers: Vec<HttpTrackerConfig>,
818 http_api: HttpApiConfig,
819 health_check_api: HealthCheckApiConfig,
820 ) -> TrackerConfig {
821 TrackerConfig::new(
822 TrackerCoreConfig::new(
823 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
824 false,
825 ),
826 udp_trackers,
827 http_trackers,
828 http_api,
829 health_check_api,
830 )
831 .expect("test values should be valid")
832 }
833
834 fn test_tracker_config_with_core(
836 core: TrackerCoreConfig,
837 udp_trackers: Vec<UdpTrackerConfig>,
838 http_trackers: Vec<HttpTrackerConfig>,
839 http_api: HttpApiConfig,
840 health_check_api: HealthCheckApiConfig,
841 ) -> TrackerConfig {
842 TrackerConfig::new(
843 core,
844 udp_trackers,
845 http_trackers,
846 http_api,
847 health_check_api,
848 )
849 .expect("test values should be valid")
850 }
851
852 fn test_private_core_config() -> TrackerCoreConfig {
854 TrackerCoreConfig::new(
855 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
856 true,
857 )
858 }
859
860 fn test_core_config_with_db(database_name: &str) -> TrackerCoreConfig {
862 TrackerCoreConfig::new(
863 DatabaseConfig::Sqlite(SqliteConfig::new(database_name).unwrap()),
864 false,
865 )
866 }
867
868 mod is_localhost_tests {
869 use super::*;
870
871 #[test]
872 fn it_should_detect_ipv4_localhost() {
873 let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap();
874 assert!(is_localhost(&addr));
875 }
876
877 #[test]
878 fn it_should_detect_ipv6_localhost() {
879 let addr: SocketAddr = "[::1]:8080".parse().unwrap();
880 assert!(is_localhost(&addr));
881 }
882
883 #[test]
884 fn it_should_not_detect_all_interfaces_ipv4() {
885 let addr: SocketAddr = "0.0.0.0:8080".parse().unwrap();
886 assert!(!is_localhost(&addr));
887 }
888
889 #[test]
890 fn it_should_not_detect_all_interfaces_ipv6() {
891 let addr: SocketAddr = "[::]:8080".parse().unwrap();
892 assert!(!is_localhost(&addr));
893 }
894
895 #[test]
896 fn it_should_not_detect_specific_ip() {
897 let addr: SocketAddr = "10.0.0.1:8080".parse().unwrap();
898 assert!(!is_localhost(&addr));
899 }
900
901 #[test]
902 fn it_should_not_detect_other_127_x_addresses() {
903 let addr: SocketAddr = "127.0.0.2:8080".parse().unwrap();
905 assert!(!is_localhost(&addr));
906 }
907 }
908
909 #[test]
910 fn it_should_create_tracker_config() {
911 let config = test_tracker_config_with_core(
912 test_private_core_config(),
913 vec![test_udp_tracker_config("0.0.0.0:6868")],
914 vec![test_http_tracker_config("0.0.0.0:7070")],
915 test_http_api_config("0.0.0.0:1212", "test_token"),
916 test_health_check_api_config("127.0.0.1:1313"),
917 );
918
919 assert_eq!(config.core().database().database_name(), "tracker.db");
920 assert!(config.core().private());
921 assert_eq!(config.udp_trackers().len(), 1);
922 assert_eq!(config.http_trackers().len(), 1);
923 }
924
925 #[test]
926 fn it_should_serialize_tracker_config() {
927 let config = test_tracker_config_with_core(
928 test_core_config_with_db("test.db"),
929 vec![],
930 vec![],
931 test_http_api_config("0.0.0.0:1212", "token123"),
932 test_health_check_api_config("127.0.0.1:1313"),
933 );
934
935 let json = serde_json::to_value(&config).unwrap();
936 assert_eq!(json["core"]["private"], false);
937 assert_eq!(json["http_api"]["admin_token"], "token123");
938 }
939
940 #[test]
941 fn it_should_create_default_tracker_config() {
942 let config = TrackerConfig::default();
943
944 assert_eq!(config.core().database().database_name(), "tracker.db");
946 assert_eq!(config.core().database().driver_name(), "sqlite3");
947
948 assert!(!config.core().private());
950
951 assert_eq!(config.udp_trackers().len(), 1);
953 assert_eq!(
954 config.udp_trackers()[0].bind_address(),
955 "0.0.0.0:6969".parse::<SocketAddr>().unwrap()
956 );
957
958 assert_eq!(config.http_trackers().len(), 1);
960 assert_eq!(
961 config.http_trackers()[0].bind_address(),
962 "0.0.0.0:7070".parse::<SocketAddr>().unwrap()
963 );
964
965 assert_eq!(
967 config.http_api().bind_address(),
968 "0.0.0.0:1212".parse::<SocketAddr>().unwrap()
969 );
970 assert_eq!(
971 config.http_api().admin_token().expose_secret(),
972 "MyAccessToken"
973 );
974 }
975
976 mod validation {
977 use super::*;
978
979 #[test]
980 fn it_should_accept_valid_configuration_with_unique_addresses() {
981 let result = TrackerConfig::new(
982 TrackerCoreConfig::new(
983 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
984 false,
985 ),
986 vec![test_udp_tracker_config("0.0.0.0:6969")],
987 vec![test_http_tracker_config("0.0.0.0:7070")],
988 test_http_api_config("0.0.0.0:1212", "token"),
989 test_health_check_api_config("127.0.0.1:1313"),
990 );
991
992 assert!(result.is_ok());
993 }
994
995 #[test]
996 fn it_should_reject_duplicate_udp_tracker_ports() {
997 let result = TrackerConfig::new(
998 TrackerCoreConfig::new(
999 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1000 false,
1001 ),
1002 vec![
1003 test_udp_tracker_config("0.0.0.0:7070"),
1004 test_udp_tracker_config("0.0.0.0:7070"),
1005 ],
1006 vec![],
1007 test_http_api_config("0.0.0.0:1212", "token"),
1008 test_health_check_api_config("127.0.0.1:1313"),
1009 );
1010
1011 assert!(result.is_err());
1012
1013 if let Err(TrackerConfigError::DuplicateSocketAddress {
1014 address,
1015 protocol,
1016 services,
1017 }) = result
1018 {
1019 assert_eq!(address, "0.0.0.0:7070".parse::<SocketAddr>().unwrap());
1020 assert_eq!(protocol, Protocol::Udp);
1021 assert_eq!(services.len(), 2);
1022 assert!(services.contains(&"UDP Tracker #1".to_string()));
1023 assert!(services.contains(&"UDP Tracker #2".to_string()));
1024 } else {
1025 panic!("Expected DuplicateSocketAddress error");
1026 }
1027 }
1028
1029 #[test]
1030 fn it_should_reject_duplicate_http_tracker_ports() {
1031 let result = TrackerConfig::new(
1032 TrackerCoreConfig::new(
1033 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1034 false,
1035 ),
1036 vec![],
1037 vec![
1038 test_http_tracker_config("0.0.0.0:7070"),
1039 test_http_tracker_config("0.0.0.0:7070"),
1040 ],
1041 test_http_api_config("0.0.0.0:1212", "token"),
1042 test_health_check_api_config("127.0.0.1:1313"),
1043 );
1044
1045 assert!(result.is_err());
1046
1047 if let Err(TrackerConfigError::DuplicateSocketAddress {
1048 address,
1049 protocol,
1050 services,
1051 }) = result
1052 {
1053 assert_eq!(address, "0.0.0.0:7070".parse::<SocketAddr>().unwrap());
1054 assert_eq!(protocol, Protocol::Tcp);
1055 assert_eq!(services.len(), 2);
1056 } else {
1057 panic!("Expected DuplicateSocketAddress error");
1058 }
1059 }
1060
1061 #[test]
1062 fn it_should_reject_http_tracker_and_api_conflict() {
1063 let result = TrackerConfig::new(
1064 TrackerCoreConfig::new(
1065 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1066 false,
1067 ),
1068 vec![],
1069 vec![test_http_tracker_config("0.0.0.0:7070")],
1070 test_http_api_config("0.0.0.0:7070", "token"),
1071 test_health_check_api_config("127.0.0.1:1313"),
1072 );
1073
1074 assert!(result.is_err());
1075
1076 if let Err(TrackerConfigError::DuplicateSocketAddress {
1077 address,
1078 protocol,
1079 services,
1080 }) = result
1081 {
1082 assert_eq!(address, "0.0.0.0:7070".parse::<SocketAddr>().unwrap());
1083 assert_eq!(protocol, Protocol::Tcp);
1084 assert_eq!(services.len(), 2);
1085 assert!(services.contains(&"HTTP Tracker #1".to_string()));
1086 assert!(services.contains(&"HTTP API".to_string()));
1087 } else {
1088 panic!("Expected DuplicateSocketAddress error");
1089 }
1090 }
1091
1092 #[test]
1093 fn it_should_reject_http_tracker_and_health_check_api_conflict() {
1094 let result = TrackerConfig::new(
1095 TrackerCoreConfig::new(
1096 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1097 false,
1098 ),
1099 vec![],
1100 vec![test_http_tracker_config("0.0.0.0:1313")],
1101 test_http_api_config("0.0.0.0:1212", "token"),
1102 test_health_check_api_config("0.0.0.0:1313"),
1103 );
1104
1105 assert!(result.is_err());
1106
1107 if let Err(TrackerConfigError::DuplicateSocketAddress {
1108 address,
1109 protocol,
1110 services,
1111 }) = result
1112 {
1113 assert_eq!(address, "0.0.0.0:1313".parse::<SocketAddr>().unwrap());
1114 assert_eq!(protocol, Protocol::Tcp);
1115 assert_eq!(services.len(), 2);
1116 assert!(services.contains(&"HTTP Tracker #1".to_string()));
1117 assert!(services.contains(&"Health Check API".to_string()));
1118 } else {
1119 panic!("Expected DuplicateSocketAddress error");
1120 }
1121 }
1122
1123 #[test]
1124 fn it_should_allow_udp_and_http_on_same_port() {
1125 let result = TrackerConfig::new(
1127 TrackerCoreConfig::new(
1128 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1129 false,
1130 ),
1131 vec![test_udp_tracker_config("0.0.0.0:7070")],
1132 vec![test_http_tracker_config("0.0.0.0:7070")],
1133 test_http_api_config("0.0.0.0:1212", "token"),
1134 test_health_check_api_config("127.0.0.1:1313"),
1135 );
1136
1137 assert!(result.is_ok());
1138 }
1139
1140 #[test]
1141 fn it_should_allow_same_port_different_ips() {
1142 let result = TrackerConfig::new(
1143 TrackerCoreConfig::new(
1144 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1145 false,
1146 ),
1147 vec![],
1148 vec![
1149 test_http_tracker_config("192.168.1.10:7070"),
1150 test_http_tracker_config("192.168.1.20:7070"),
1151 ],
1152 test_http_api_config("0.0.0.0:1212", "token"),
1153 test_health_check_api_config("127.0.0.1:1313"),
1154 );
1155
1156 assert!(result.is_ok());
1157 }
1158
1159 #[test]
1160 fn it_should_provide_clear_error_message_with_fix_instructions() {
1161 let result = TrackerConfig::new(
1162 TrackerCoreConfig::new(
1163 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1164 false,
1165 ),
1166 vec![],
1167 vec![test_http_tracker_config("0.0.0.0:7070")],
1168 test_http_api_config("0.0.0.0:7070", "token"),
1169 test_health_check_api_config("127.0.0.1:1313"),
1170 );
1171
1172 let error = result.unwrap_err();
1173 let error_message = error.to_string();
1174
1175 assert!(error_message.contains("Socket address conflict"));
1177 assert!(error_message.contains("'HTTP Tracker #1'"));
1178 assert!(error_message.contains("'HTTP API'"));
1179 assert!(error_message.contains("0.0.0.0:7070"));
1180 assert!(error_message.contains("TCP"));
1181 assert!(error_message.contains("Tip: Assign different port numbers"));
1182
1183 let help = error.help();
1185 assert!(help.contains("Socket Address Conflict - Detailed Troubleshooting"));
1186 assert!(help.contains("Conflicting services:"));
1187 assert!(help.contains("HTTP Tracker #1"));
1188 assert!(help.contains("HTTP API"));
1189 assert!(help.contains("Why this fails:"));
1190 assert!(help.contains("How to fix:"));
1191 assert!(help.contains("docs/external-issues/tracker/udp-tcp-port-sharing-allowed.md"));
1192 }
1193 }
1194
1195 mod localhost_with_tls_validation {
1196 use super::*;
1197
1198 fn base_config() -> TrackerConfig {
1199 test_tracker_config(
1200 vec![],
1201 vec![],
1202 test_http_api_config("0.0.0.0:1212", "token"),
1203 test_health_check_api_config("127.0.0.1:1313"),
1204 )
1205 }
1206
1207 #[test]
1214 fn it_should_allow_localhost_without_tls() {
1215 let config = base_config();
1217 assert_eq!(config.http_api().bind_address().port(), 1212);
1219 }
1220
1221 #[test]
1222 fn it_should_allow_non_localhost_with_tls() {
1223 let domain = crate::shared::DomainName::new("api.tracker.local").unwrap();
1224 let config = TrackerConfig::new(
1225 TrackerCoreConfig::new(
1226 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1227 false,
1228 ),
1229 vec![],
1230 vec![],
1231 test_http_api_config_with_tls("0.0.0.0:1212", "token", Some(domain), true),
1232 test_health_check_api_config("127.0.0.1:1313"),
1233 )
1234 .expect("valid config");
1235
1236 assert!(config.http_api().use_tls_proxy());
1237 }
1238 }
1239
1240 mod port_derivation {
1245 use super::*;
1246
1247 fn default_core() -> TrackerCoreConfig {
1248 TrackerCoreConfig::new(
1249 DatabaseConfig::Sqlite(SqliteConfig::new("tracker.db").unwrap()),
1250 false,
1251 )
1252 }
1253
1254 #[test]
1255 fn it_should_expose_udp_ports_always() {
1256 let config = TrackerConfig::new(
1258 default_core(),
1259 vec![
1260 test_udp_tracker_config("0.0.0.0:6969"),
1261 test_udp_tracker_config("0.0.0.0:6868"),
1262 ],
1263 vec![],
1264 test_http_api_config("0.0.0.0:1212", "token"),
1265 test_health_check_api_config("127.0.0.1:1313"),
1266 )
1267 .unwrap();
1268
1269 let ports = config.derive_ports();
1270 let udp_ports: Vec<_> = ports
1271 .iter()
1272 .filter(|p| p.protocol() == Protocol::Udp)
1273 .collect();
1274
1275 assert_eq!(udp_ports.len(), 2);
1276 assert_eq!(udp_ports[0].host_port(), 6969);
1277 assert_eq!(udp_ports[1].host_port(), 6868);
1278 }
1279
1280 #[test]
1281 fn it_should_expose_http_ports_without_tls() {
1282 let config = TrackerConfig::new(
1284 default_core(),
1285 vec![],
1286 vec![
1287 test_http_tracker_config("0.0.0.0:7070"),
1288 test_http_tracker_config("0.0.0.0:8080"),
1289 ],
1290 test_http_api_config("0.0.0.0:1212", "token"),
1291 test_health_check_api_config("127.0.0.1:1313"),
1292 )
1293 .unwrap();
1294
1295 let ports = config.derive_ports();
1296 let tcp_ports: Vec<_> = ports
1298 .iter()
1299 .filter(|p| p.protocol() == Protocol::Tcp)
1300 .collect();
1301
1302 assert_eq!(tcp_ports.len(), 3);
1303 assert!(tcp_ports.iter().any(|p| p.host_port() == 7070));
1304 assert!(tcp_ports.iter().any(|p| p.host_port() == 8080));
1305 }
1306
1307 #[test]
1308 fn it_should_not_expose_http_ports_with_tls() {
1309 let domain = crate::shared::DomainName::new("tracker.example.com").unwrap();
1311 let config = TrackerConfig::new(
1312 default_core(),
1313 vec![],
1314 vec![test_http_tracker_config_with_tls(
1315 "0.0.0.0:7070",
1316 Some(domain),
1317 true,
1318 )],
1319 test_http_api_config("0.0.0.0:1212", "token"),
1320 test_health_check_api_config("127.0.0.1:1313"),
1321 )
1322 .unwrap();
1323
1324 let ports = config.derive_ports();
1325 assert!(ports.iter().all(|p| p.host_port() != 7070));
1327 }
1328
1329 #[test]
1330 fn it_should_expose_api_port_without_tls() {
1331 let config = TrackerConfig::new(
1333 default_core(),
1334 vec![],
1335 vec![],
1336 test_http_api_config("0.0.0.0:1212", "token"),
1337 test_health_check_api_config("127.0.0.1:1313"),
1338 )
1339 .unwrap();
1340
1341 let ports = config.derive_ports();
1342 let api_port = ports.iter().find(|p| p.host_port() == 1212);
1343
1344 assert!(api_port.is_some());
1345 assert_eq!(
1346 api_port.unwrap().description(),
1347 "HTTP API (stats/whitelist)"
1348 );
1349 }
1350
1351 #[test]
1352 fn it_should_not_expose_api_port_with_tls() {
1353 let domain = crate::shared::DomainName::new("api.example.com").unwrap();
1355 let config = TrackerConfig::new(
1356 default_core(),
1357 vec![],
1358 vec![],
1359 test_http_api_config_with_tls("0.0.0.0:1212", "token", Some(domain), true),
1360 test_health_check_api_config("127.0.0.1:1313"),
1361 )
1362 .unwrap();
1363
1364 let ports = config.derive_ports();
1365 assert!(ports.iter().all(|p| p.host_port() != 1212));
1367 }
1368
1369 #[test]
1370 fn it_should_return_empty_when_all_ports_hidden_by_tls() {
1371 let api_domain = crate::shared::DomainName::new("api.example.com").unwrap();
1373 let tracker_domain = crate::shared::DomainName::new("tracker.example.com").unwrap();
1374
1375 let config = TrackerConfig::new(
1376 default_core(),
1377 vec![], vec![test_http_tracker_config_with_tls(
1379 "0.0.0.0:7070",
1380 Some(tracker_domain),
1381 true,
1382 )],
1383 test_http_api_config_with_tls("0.0.0.0:1212", "token", Some(api_domain), true),
1384 test_health_check_api_config("127.0.0.1:1313"),
1385 )
1386 .unwrap();
1387
1388 let ports = config.derive_ports();
1389 assert!(ports.is_empty());
1390 }
1391
1392 #[test]
1393 fn it_should_include_descriptions_for_all_ports() {
1394 let config = TrackerConfig::default();
1395
1396 let ports = config.derive_ports();
1397
1398 for port in &ports {
1399 assert!(!port.description().is_empty());
1400 }
1401 }
1402 }
1403}