upnp.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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. //! UPnP IGD port mapping implementation
  19. //!
  20. //! This module provides UPnP Internet Gateway Device (IGD) port mapping
  21. //! with automatic lease renewal and persistent retry for roaming support.
  22. use std::{
  23. collections::hash_map::DefaultHasher,
  24. hash::{Hash, Hasher},
  25. sync::Arc,
  26. time::Duration,
  27. };
  28. use async_trait::async_trait;
  29. use oxy_upnp_igd::{add_port_mapping_lazy, Protocol, RenewalHandle};
  30. use smol::lock::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
  31. use tracing::error;
  32. use url::Url;
  33. use crate::{
  34. net::settings::Settings,
  35. system::{sleep, ExecutorPtr, StoppableTask, StoppableTaskPtr},
  36. util::logger::verbose,
  37. Error, Result,
  38. };
  39. /// Trait for port mapping protocols (UPnP, NAT-PMP, PCP)
  40. ///
  41. /// Each protocol runs its own persistent task that:
  42. /// 1. Attempts to discover a gateway
  43. /// 2. Creates port mappings when gateway is found
  44. /// 3. Periodically refreshes the external address
  45. /// 4. Retries discovery on failures (supports roaming)
  46. pub trait PortMapping: Send + Sync {
  47. /// Start the port mapping protocol - runs forever with retries
  48. fn start(
  49. self: Arc<Self>,
  50. settings: Arc<AsyncRwLock<Settings>>,
  51. executor: ExecutorPtr,
  52. ) -> Result<()>;
  53. /// Stop the port mapping protocol
  54. fn stop(self: Arc<Self>);
  55. }
  56. /// UPnP port mapping configuration
  57. #[derive(Clone, Debug)]
  58. pub struct UpnpConfig {
  59. /// Port mapping lease duration in seconds
  60. pub lease_duration: u32,
  61. /// Gateway discovery timeout in seconds
  62. pub discovery_timeout_secs: u64,
  63. /// Description for port mapping (visible in router admin panel)
  64. pub mapping_description: String,
  65. /// External address refresh interval in seconds
  66. pub ext_addr_refresh: u64,
  67. /// How often to retry discovery if gateway not found (roaming support)
  68. pub retry_interval_secs: u64,
  69. }
  70. impl Default for UpnpConfig {
  71. fn default() -> Self {
  72. Self {
  73. lease_duration: 300,
  74. discovery_timeout_secs: 3,
  75. mapping_description: "DarkFi".to_string(),
  76. ext_addr_refresh: 120,
  77. retry_interval_secs: 60,
  78. }
  79. }
  80. }
  81. /// UPnP IGD port mapping protocol
  82. ///
  83. /// Maintains a persistent task that:
  84. /// - Discovers UPnP gateway
  85. /// - Creates port mappings
  86. /// - Periodically refreshes the external address
  87. /// - Retries discovery on failures (supports roaming devices)
  88. pub struct UpnpPortMapping {
  89. config: UpnpConfig,
  90. internal_endpoint: Url,
  91. handle: AsyncMutex<Option<RenewalHandle>>,
  92. task: StoppableTaskPtr,
  93. }
  94. impl UpnpPortMapping {
  95. /// Create a new UPnP port mapping instance
  96. pub fn new(config: UpnpConfig, internal_endpoint: Url) -> Self {
  97. Self {
  98. config,
  99. internal_endpoint,
  100. handle: AsyncMutex::new(None),
  101. task: StoppableTask::new(),
  102. }
  103. }
  104. /// Main protocol loop - runs forever with retries
  105. async fn run(&self, settings: Arc<AsyncRwLock<Settings>>, ex: &ExecutorPtr) -> Result<()> {
  106. loop {
  107. if self.try_create_mapping(ex).await.is_err() {
  108. verbose!(
  109. target: "net::upnp",
  110. "[P2P] UPnP: Gateway discovery failed, retrying in {}s",
  111. self.config.retry_interval_secs
  112. );
  113. sleep(self.config.retry_interval_secs).await;
  114. continue;
  115. }
  116. verbose!(
  117. target: "net::upnp",
  118. "[P2P] UPnP: Gateway discovered, mapping active for {}",
  119. self.internal_endpoint
  120. );
  121. if self.run_refresh_loop(settings.clone()).await.is_err() {
  122. verbose!(
  123. target: "net::upnp",
  124. "[P2P] UPnP: Gateway lost, retrying discovery in {}s",
  125. self.config.retry_interval_secs
  126. );
  127. sleep(self.config.retry_interval_secs).await;
  128. continue;
  129. }
  130. unreachable!("UPnP refresh loop should never complete normally");
  131. }
  132. }
  133. /// Attempt to discover gateway and create initial port mapping
  134. async fn try_create_mapping(&self, ex: &ExecutorPtr) -> Result<()> {
  135. let protocol = match self.internal_endpoint.scheme() {
  136. "tcp" | "tcp+tls" => Protocol::TCP,
  137. "quic" => Protocol::UDP,
  138. s => {
  139. verbose!(
  140. target: "net::upnp",
  141. "[P2P] UPnP: Unsupported scheme '{s}', skipping"
  142. );
  143. return Err(Error::NetworkServiceStopped);
  144. }
  145. };
  146. // UPnP IGD port mapping is IPv4-only
  147. let is_ipv4 = match self.internal_endpoint.host() {
  148. Some(url::Host::Ipv4(_)) => true,
  149. Some(url::Host::Ipv6(_)) => false,
  150. // Treating domains as IPv4 is safe and generally useful
  151. Some(url::Host::Domain(_)) => true,
  152. None => false,
  153. };
  154. if !is_ipv4 {
  155. verbose!(
  156. target: "net::upnp",
  157. "[P2P] UPnP: Skipping IPv6 endpoint {} (IGD pinhole not implemented)",
  158. self.internal_endpoint
  159. );
  160. return Err(Error::NetworkServiceStopped);
  161. }
  162. let internal_port = match self.internal_endpoint.port() {
  163. Some(port) => port,
  164. None => {
  165. verbose!(
  166. target: "net::upnp",
  167. "[P2P] UPnP: Invalid endpoint (missing port): {}",
  168. self.internal_endpoint
  169. );
  170. return Err(Error::NetworkServiceStopped);
  171. }
  172. };
  173. let timeout = Duration::from_secs(self.config.discovery_timeout_secs);
  174. verbose!(
  175. target: "net::upnp",
  176. "[P2P] UPnP: Attempting port mapping for internal port {}",
  177. internal_port
  178. );
  179. // This will return immediately with a lazy handle
  180. let handle = add_port_mapping_lazy(
  181. ex.clone(),
  182. internal_port,
  183. protocol,
  184. &self.config.mapping_description,
  185. self.config.lease_duration,
  186. timeout,
  187. )
  188. .await?;
  189. *self.handle.lock().await = Some(handle);
  190. Ok(())
  191. }
  192. /// Refresh loop - updates external address periodically
  193. async fn run_refresh_loop(&self, settings: Arc<AsyncRwLock<Settings>>) -> Result<()> {
  194. loop {
  195. sleep(self.config.ext_addr_refresh).await;
  196. let Some(external_url) = self.get_external_address().await else {
  197. verbose!(
  198. target: "net::upnp",
  199. "[P2P] UPnP: Gateway no longer available"
  200. );
  201. return Err(Error::NetworkServiceStopped);
  202. };
  203. // Update settings with new external address
  204. let mut settings = settings.write().await;
  205. // Remove our old address (avoid duplicates)
  206. let internal_id = format_address_id(&self.internal_endpoint, "upnp");
  207. settings.external_addrs.retain(|addr: &Url| {
  208. if let Some(query) = addr.query() {
  209. !query.contains(internal_id.as_str())
  210. } else {
  211. true // Keep manually configured addresses
  212. }
  213. });
  214. // Add new external address
  215. settings.external_addrs.push(external_url.clone());
  216. verbose!(
  217. target: "net::upnp",
  218. "[P2P] UPnP: Updated external address: {}",
  219. external_url
  220. );
  221. }
  222. }
  223. /// Get current external address from UPnP handle
  224. async fn get_external_address(&self) -> Option<Url> {
  225. let handle = self.handle.lock().await;
  226. let handle = handle.as_ref()?;
  227. let external_ip = handle.external_ip().await;
  228. if external_ip.is_unspecified() {
  229. return None;
  230. }
  231. let external_port = handle.external_port();
  232. if external_port == 0 {
  233. return None;
  234. }
  235. let scheme = self.internal_endpoint.scheme();
  236. let internal_id = format_address_id(&self.internal_endpoint, "upnp");
  237. Url::parse(&format!(
  238. "{}://{}:{}?source=upnp&{}",
  239. scheme, external_ip, external_port, internal_id
  240. ))
  241. .ok()
  242. }
  243. }
  244. #[async_trait]
  245. impl PortMapping for UpnpPortMapping {
  246. fn start(self: Arc<Self>, settings: Arc<AsyncRwLock<Settings>>, ex: ExecutorPtr) -> Result<()> {
  247. let self_ = self.clone();
  248. let settings_ = settings.clone();
  249. let ex_ = ex.clone();
  250. self.task.clone().start(
  251. async move { self_.run(settings_, &ex_).await },
  252. |result| async move {
  253. match result {
  254. Ok(()) => {
  255. // Should never complete normally
  256. verbose!("[P2P] UPnP task completed unexpectedly");
  257. }
  258. Err(Error::NetworkServiceStopped) => {
  259. // Expected when stopping
  260. }
  261. Err(e) => {
  262. verbose!("[P2P] UPnP task error: {e}");
  263. }
  264. }
  265. },
  266. Error::NetworkServiceStopped,
  267. ex,
  268. );
  269. Ok(())
  270. }
  271. fn stop(self: Arc<Self>) {
  272. // Stop the task (synchronous, signals the task to stop)
  273. self.task.stop_nowait();
  274. // Handle dropped - mapping expires naturally
  275. verbose!(
  276. target: "net::upnp",
  277. "[P2P] UPnP: Stopped port mapping for {}",
  278. self.internal_endpoint
  279. );
  280. }
  281. }
  282. /// Format an identifier for this listener + protocol combination
  283. ///
  284. /// This utility is shared across all port mapping protocols (UPnP, NAT-PMP, PCP)
  285. /// to create consistent, unique identifiers for external addresses.
  286. pub fn format_address_id(endpoint: &Url, protocol: &str) -> String {
  287. // Hash the endpoint URL to create a unique alphanumeric identifier
  288. let mut hasher = DefaultHasher::new();
  289. endpoint.hash(&mut hasher);
  290. let hash = hasher.finish();
  291. format!("{}_cookie={:016x}", protocol, hash)
  292. }
  293. /// Create UPnP port mapping from URL query parameters
  294. pub fn create_upnp_from_url(url: &Url) -> Option<Arc<dyn PortMapping>> {
  295. // Check if UPnP is explicitly enabled
  296. if !url.query_pairs().any(|(key, value)| key == "upnp_igd" && value == "true") {
  297. return None;
  298. }
  299. // Parse configuration from URL query parameters using safe URL library methods
  300. let mut config = UpnpConfig::default();
  301. for (key, value) in url.query_pairs() {
  302. match key.as_ref() {
  303. "upnp_igd_lease_duration" => {
  304. if let Ok(val) = value.parse::<u32>() {
  305. config.lease_duration = val;
  306. }
  307. }
  308. "upnp_igd_timeout" => {
  309. if let Ok(val) = value.parse::<u64>() {
  310. config.discovery_timeout_secs = val;
  311. }
  312. }
  313. "upnp_igd_description" => {
  314. config.mapping_description = value.into_owned();
  315. }
  316. "upnp_igd_ext_addr_refresh" => {
  317. if let Ok(val) = value.parse::<u64>() {
  318. config.ext_addr_refresh = val;
  319. }
  320. }
  321. _ => {}
  322. }
  323. }
  324. Some(Arc::new(UpnpPortMapping::new(config, url.clone())))
  325. }
  326. /// Initialize port mappings from URL query parameters.
  327. ///
  328. /// This function parses the endpoint URL for port mapping configuration,
  329. /// creates the appropriate port mapping instances, and starts them.
  330. /// Each port mapping runs its own persistent task for lease renewal
  331. /// and external address updates.
  332. ///
  333. /// # Examples
  334. /// ```text
  335. /// // Enable UPnP with defaults
  336. /// ?upnp_igd=true
  337. ///
  338. /// // UPnP with custom settings
  339. /// ?upnp_igd=true&upnp_igd_lease_duration=600
  340. ///
  341. /// // Multiple protocols
  342. /// ?upnp_igd=true&pcp=true
  343. /// ```
  344. ///
  345. /// # Arguments
  346. /// * `endpoint` - The actual endpoint URL with query parameters and
  347. /// *assigned port*
  348. /// * `settings` - P2P settings for updating external addresses
  349. /// * `ex` - Executor for running async tasks
  350. ///
  351. /// # Returns
  352. /// A vector of started port mappings (they auto-clean on drop)
  353. pub fn setup_port_mappings(
  354. actual_endpoint: &Url,
  355. settings: Arc<AsyncRwLock<Settings>>,
  356. ex: ExecutorPtr,
  357. ) -> Vec<Arc<dyn PortMapping>> {
  358. let Some(mapping) = create_upnp_from_url(actual_endpoint) else { return vec![] };
  359. if let Err(e) = Arc::clone(&mapping).start(settings.clone(), ex.clone()) {
  360. verbose!(
  361. target: "net::upnp",
  362. "[P2P] UPnP port mapping: Failed to start for {}: {e}",
  363. actual_endpoint
  364. );
  365. return vec![]
  366. }
  367. verbose!(
  368. target: "net::upnp",
  369. "[P2P] UPnP: Port mapping started for {}",
  370. actual_endpoint
  371. );
  372. vec![mapping]
  373. // Future: Add NAT-PMP, PCP here with similar patterns
  374. }