connector.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{
  19. future::Future,
  20. io,
  21. sync::{atomic::Ordering, Arc},
  22. time::Duration,
  23. };
  24. use futures::{
  25. future::{select, Either},
  26. pin_mut,
  27. };
  28. use smol::lock::RwLock as AsyncRwLock;
  29. use url::Url;
  30. use super::{
  31. channel::{Channel, ChannelPtr},
  32. session::SessionWeakPtr,
  33. settings::Settings,
  34. transport::Dialer,
  35. };
  36. use crate::{net::hosts::HostContainer, system::CondVar, util::logger::verbose, Error, Result};
  37. type DialRoute = (Url, bool, Duration);
  38. type DialFailures = Vec<(Url, io::Error)>;
  39. #[derive(Debug)]
  40. enum DialRoutesError {
  41. Stopped(Url),
  42. Failed(DialFailures),
  43. }
  44. fn partition_blacklisted_endpoints<F>(
  45. endpoints: Vec<(Url, bool)>,
  46. mut is_blacklisted: F,
  47. ) -> (Vec<(Url, bool)>, Vec<Url>)
  48. where
  49. F: FnMut(&Url) -> bool,
  50. {
  51. let mut allowed = vec![];
  52. let mut blocked = vec![];
  53. for (endpoint, mixed_transport) in endpoints {
  54. if is_blacklisted(&endpoint) {
  55. blocked.push(endpoint);
  56. } else {
  57. allowed.push((endpoint, mixed_transport));
  58. }
  59. }
  60. (allowed, blocked)
  61. }
  62. fn build_dial_routes(endpoints: Vec<(Url, bool)>, settings: &Settings) -> Vec<DialRoute> {
  63. endpoints
  64. .into_iter()
  65. .map(|(endpoint, mixed_transport)| {
  66. let timeout = Duration::from_secs(settings.outbound_connect_timeout(endpoint.scheme()));
  67. (endpoint, mixed_transport, timeout)
  68. })
  69. .collect()
  70. }
  71. async fn try_dial_routes<T, F, Fut>(
  72. routes: Vec<DialRoute>,
  73. stop_signal: &CondVar,
  74. mut dial: F,
  75. ) -> std::result::Result<(Url, bool, T), DialRoutesError>
  76. where
  77. F: FnMut(Url, Duration) -> Fut,
  78. Fut: Future<Output = io::Result<T>>,
  79. {
  80. let mut failures = vec![];
  81. for (endpoint, mixed_transport, timeout) in routes {
  82. let stop_fut = stop_signal.wait();
  83. let dial_fut = dial(endpoint.clone(), timeout);
  84. pin_mut!(stop_fut);
  85. pin_mut!(dial_fut);
  86. match select(dial_fut, stop_fut).await {
  87. Either::Left((Ok(stream), _)) => return Ok((endpoint, mixed_transport, stream)),
  88. Either::Left((Err(err), _)) => failures.push((endpoint, err)),
  89. Either::Right((_, _)) => return Err(DialRoutesError::Stopped(endpoint)),
  90. }
  91. }
  92. Err(DialRoutesError::Failed(failures))
  93. }
  94. fn sanitized_url(url: &Url) -> String {
  95. let mut sanitized = url.clone();
  96. let _ = sanitized.set_password(None);
  97. let _ = sanitized.set_username("");
  98. sanitized.set_query(None);
  99. sanitized.set_fragment(None);
  100. sanitized.to_string()
  101. }
  102. fn route_description(canonical: &Url, endpoint: &Url) -> String {
  103. format!("peer [{}] via route [{}]", sanitized_url(canonical), sanitized_url(endpoint))
  104. }
  105. fn summarize_failures(failures: &DialFailures) -> String {
  106. failures
  107. .iter()
  108. .map(|(endpoint, err)| format!("{} ({:?})", sanitized_url(endpoint), err.kind()))
  109. .collect::<Vec<_>>()
  110. .join(", ")
  111. }
  112. /// Create outbound socket connections
  113. pub struct Connector {
  114. /// P2P settings
  115. settings: Arc<AsyncRwLock<Settings>>,
  116. /// Weak pointer to the session
  117. pub session: SessionWeakPtr,
  118. /// Stop signal that aborts the connector if received.
  119. stop_signal: CondVar,
  120. }
  121. impl Connector {
  122. /// Create a new connector with given network settings
  123. pub fn new(settings: Arc<AsyncRwLock<Settings>>, session: SessionWeakPtr) -> Self {
  124. Self { settings, session, stop_signal: CondVar::new() }
  125. }
  126. /// Establish an outbound connection
  127. pub async fn connect(&self, url: &Url) -> Result<(Url, ChannelPtr)> {
  128. let hosts = self.session.upgrade().unwrap().p2p().hosts();
  129. // A canonical blacklist match blocks the peer regardless of route.
  130. if hosts.is_blacklisted(url) {
  131. let url = sanitized_url(url);
  132. verbose!(target: "net::connector::connect", "Peer {url} is blacklisted");
  133. return Err(Error::ConnectFailed(format!("[{url}]: Peer is blacklisted")));
  134. }
  135. let settings = self.settings.read().await;
  136. let datastore = settings.p2p_datastore.clone();
  137. let i2p_socks5_proxy = settings.i2p_socks5_proxy.clone();
  138. let endpoints = HostContainer::resolve_dial_endpoints(
  139. url,
  140. &settings.active_profiles,
  141. &settings.mixed_profiles,
  142. &settings.tor_socks5_proxy,
  143. &settings.nym_socks5_proxy,
  144. );
  145. if endpoints.is_empty() {
  146. return Err(Error::UnsupportedTransport(url.scheme().to_string()))
  147. }
  148. // A derived endpoint match blocks only that route, allowing a safe
  149. // alternative transport to be tried when one is available.
  150. let (endpoints, blocked) =
  151. partition_blacklisted_endpoints(endpoints, |endpoint| hosts.is_blacklisted(endpoint));
  152. for endpoint in blocked {
  153. verbose!(
  154. target: "net::connector::connect",
  155. "Skipping blacklisted connection route {}",
  156. sanitized_url(&endpoint),
  157. );
  158. }
  159. if endpoints.is_empty() {
  160. return Err(Error::ConnectFailed(format!(
  161. "[{}]: All connection routes are blacklisted",
  162. sanitized_url(url)
  163. )))
  164. }
  165. let routes = build_dial_routes(endpoints, &settings);
  166. drop(settings);
  167. let canonical = url.clone();
  168. let result = try_dial_routes(routes, &self.stop_signal, |endpoint, timeout| {
  169. let datastore = datastore.clone();
  170. let i2p_socks5_proxy = i2p_socks5_proxy.clone();
  171. let canonical = canonical.clone();
  172. async move {
  173. verbose!(
  174. target: "net::connector::connect",
  175. "[P2P] Connecting {}",
  176. route_description(&canonical, &endpoint),
  177. );
  178. let dialer = Dialer::new(endpoint, datastore, Some(i2p_socks5_proxy), true).await?;
  179. dialer.dial(Some(timeout)).await
  180. }
  181. })
  182. .await;
  183. match result {
  184. Ok((endpoint, mixed_transport, ptstream)) => {
  185. let channel = Channel::new(
  186. ptstream,
  187. Some(endpoint.clone()),
  188. url.clone(),
  189. self.session.clone(),
  190. mixed_transport,
  191. )
  192. .await;
  193. Ok((endpoint, channel))
  194. }
  195. Err(DialRoutesError::Failed(failures)) => {
  196. // If we get ENETUNREACH, we don't have IPv6 connectivity so note it down.
  197. if failures.iter().any(|(_, err)| err.raw_os_error() == Some(libc::ENETUNREACH)) {
  198. hosts.ipv6_available.store(false, Ordering::SeqCst);
  199. }
  200. Err(Error::ConnectFailed(format!(
  201. "All connection routes failed: {}",
  202. summarize_failures(&failures)
  203. )))
  204. }
  205. Err(DialRoutesError::Stopped(endpoint)) => {
  206. Err(Error::ConnectorStopped(format!("[{}]", sanitized_url(&endpoint))))
  207. }
  208. }
  209. }
  210. pub(crate) fn stop(&self) {
  211. self.stop_signal.notify()
  212. }
  213. }
  214. #[cfg(test)]
  215. mod tests {
  216. use std::sync::{Arc, Mutex};
  217. use crate::net::settings::NetworkProfile;
  218. use super::*;
  219. fn route(url: &str, timeout: u64) -> DialRoute {
  220. (Url::parse(url).unwrap(), true, Duration::from_secs(timeout))
  221. }
  222. #[test]
  223. fn test_route_description_reports_effective_transport_without_credentials() {
  224. let canonical = Url::parse("tcp+tls://irc.dark.fi:9600").unwrap();
  225. let endpoint = Url::parse("tor+tls://alice:secret@irc.dark.fi:9600?token=hidden").unwrap();
  226. let description = route_description(&canonical, &endpoint);
  227. assert_eq!(
  228. description,
  229. "peer [tcp+tls://irc.dark.fi:9600/] via route [tor+tls://irc.dark.fi:9600/]"
  230. );
  231. }
  232. #[test]
  233. fn test_mixed_routes_skip_blacklisted_endpoint() {
  234. let endpoints = vec![
  235. (Url::parse("tor+tls://peer.example:28880").unwrap(), true),
  236. (Url::parse("nym+tls://peer.example:28880").unwrap(), true),
  237. ];
  238. let (allowed, blocked) =
  239. partition_blacklisted_endpoints(endpoints, |url| url.scheme() == "tor+tls");
  240. assert_eq!(allowed.len(), 1);
  241. assert_eq!(allowed[0].0.scheme(), "nym+tls");
  242. assert_eq!(blocked.len(), 1);
  243. assert_eq!(blocked[0].scheme(), "tor+tls");
  244. }
  245. #[test]
  246. fn test_mixed_routes_reject_all_blacklisted_endpoints() {
  247. let endpoints = vec![
  248. (Url::parse("tor://peer.example:28880").unwrap(), true),
  249. (Url::parse("nym://peer.example:28880").unwrap(), true),
  250. ];
  251. let (allowed, blocked) = partition_blacklisted_endpoints(endpoints, |_| true);
  252. assert!(allowed.is_empty());
  253. assert_eq!(blocked.len(), 2);
  254. }
  255. #[test]
  256. fn test_dial_routes_use_endpoint_profile_timeouts() {
  257. let mut settings = Settings::default();
  258. settings.profiles.insert(
  259. "tor".to_string(),
  260. NetworkProfile { outbound_connect_timeout: 3, ..Default::default() },
  261. );
  262. settings.profiles.insert(
  263. "nym".to_string(),
  264. NetworkProfile { outbound_connect_timeout: 7, ..Default::default() },
  265. );
  266. let endpoints = vec![
  267. (Url::parse("tor://peer.example:28880").unwrap(), true),
  268. (Url::parse("nym://peer.example:28880").unwrap(), true),
  269. ];
  270. let routes = build_dial_routes(endpoints, &settings);
  271. assert_eq!(routes[0].2, Duration::from_secs(3));
  272. assert_eq!(routes[1].2, Duration::from_secs(7));
  273. }
  274. #[test]
  275. fn test_dial_routes_falls_back_after_failure() {
  276. smol::block_on(async {
  277. let attempts = Arc::new(Mutex::new(vec![]));
  278. let recorded = attempts.clone();
  279. let routes = vec![
  280. route("socks5://proxy-one.example:9050/peer.example:28880", 3),
  281. route("socks5://proxy-two.example:9050/peer.example:28880", 7),
  282. ];
  283. let result = try_dial_routes(routes, &CondVar::new(), move |endpoint, timeout| {
  284. let recorded = recorded.clone();
  285. async move {
  286. recorded.lock().unwrap().push((endpoint.clone(), timeout));
  287. if endpoint.host_str() == Some("proxy-one.example") {
  288. return Err(io::Error::from(io::ErrorKind::ConnectionRefused))
  289. }
  290. Ok(42)
  291. }
  292. })
  293. .await
  294. .unwrap();
  295. assert_eq!(result.0.host_str(), Some("proxy-two.example"));
  296. assert_eq!(result.2, 42);
  297. assert_eq!(
  298. attempts
  299. .lock()
  300. .unwrap()
  301. .iter()
  302. .map(|(endpoint, timeout)| (endpoint.host_str().unwrap().to_string(), *timeout))
  303. .collect::<Vec<_>>(),
  304. [
  305. ("proxy-one.example".to_string(), Duration::from_secs(3)),
  306. ("proxy-two.example".to_string(), Duration::from_secs(7)),
  307. ]
  308. );
  309. });
  310. }
  311. #[test]
  312. fn test_dial_routes_reports_all_failures_without_credentials() {
  313. smol::block_on(async {
  314. let routes = vec![
  315. route("socks5://alice:secret@proxy-one.example:9050/peer.example:28880", 3),
  316. route("socks5://bob:hidden@proxy-two.example:9050/peer.example:28880", 7),
  317. ];
  318. let Err(DialRoutesError::Failed(failures)) =
  319. try_dial_routes(routes, &CondVar::new(), |_, _| async {
  320. Err::<(), _>(io::Error::from(io::ErrorKind::ConnectionRefused))
  321. })
  322. .await
  323. else {
  324. panic!("all routes should fail")
  325. };
  326. let summary = summarize_failures(&failures);
  327. assert!(summary.contains("proxy-one.example"));
  328. assert!(summary.contains("proxy-two.example"));
  329. assert!(!summary.contains("alice"));
  330. assert!(!summary.contains("secret"));
  331. assert!(!summary.contains("bob"));
  332. assert!(!summary.contains("hidden"));
  333. });
  334. }
  335. #[test]
  336. fn test_dial_routes_stops_without_trying_fallback() {
  337. smol::block_on(async {
  338. let stop_signal = CondVar::new();
  339. stop_signal.notify();
  340. let attempts = Arc::new(Mutex::new(vec![]));
  341. let recorded = attempts.clone();
  342. let routes = vec![
  343. route("socks5://proxy-one.example:9050/peer.example:28880", 3),
  344. route("socks5://proxy-two.example:9050/peer.example:28880", 7),
  345. ];
  346. let result = try_dial_routes(routes, &stop_signal, move |endpoint, _| {
  347. let recorded = recorded.clone();
  348. async move {
  349. recorded.lock().unwrap().push(endpoint);
  350. futures::future::pending::<io::Result<()>>().await
  351. }
  352. })
  353. .await;
  354. assert!(matches!(result, Err(DialRoutesError::Stopped(_))));
  355. assert_eq!(attempts.lock().unwrap().len(), 1);
  356. });
  357. }
  358. }