easy_upnp/
lib.rs

1//! # easy-upnp
2//!
3//! [![badge github]][url github]
4//! [![badge crates.io]][url crates.io]
5//! [![badge docs.rs]][url docs.rs]
6//! [![badge license]][url license]
7//!
8//! [badge github]: https://img.shields.io/badge/github-FloGa%2Fupnp--daemon-green
9//! [badge crates.io]: https://img.shields.io/crates/v/easy-upnp
10//! [badge docs.rs]: https://img.shields.io/docsrs/easy-upnp
11//! [badge license]: https://img.shields.io/crates/l/easy-upnp
12//!
13//! [url github]: https://github.com/FloGa/upnp-daemon/crates/easy-upnp
14//! [url crates.io]: https://crates.io/crates/easy-upnp
15//! [url docs.rs]: https://docs.rs/easy-upnp
16//! [url license]:
17//! https://github.com/FloGa/upnp-daemon/blob/develop/crates/easy-upnp/LICENSE
18//!
19//! Easily open and close UPnP ports.
20//!
21//! A minimalistic wrapper around [IGD] to open and close network ports via
22//! [UPnP]. Mainly this library is used in the CLI application [`upnp-daemon`],
23//! but it can also be used as a library in other crates that just want to open
24//! and close ports with minimal possible configuration.
25//!
26//! [IGD]: https://docs.rs/igd/
27//! [UPnP]: https://en.wikipedia.org/wiki/Universal_Plug_and_Play
28//! [`upnp-daemon`]: https://github.com/FloGa/upnp-daemon
29//!
30//! ## Example
31//!
32//! Here is a hands-on example to demonstrate the usage. It will add some ports
33//! and immediately remove them again.
34//!
35//! ```rust no_run
36//! use std::error::Error;
37//! use log::error;
38//! use easy_upnp::{add_ports, delete_ports, Ipv4Cidr, PortMappingProtocol, UpnpConfig};
39//!
40//! fn get_configs() -> Result<[UpnpConfig; 3], Box<dyn Error>> {
41//!     let config_no_address = UpnpConfig {
42//!         address: None,
43//!         port: 80,
44//!         protocol: PortMappingProtocol::TCP,
45//!         duration: 3600,
46//!         comment: "Webserver".to_string(),
47//!     };
48//!
49//!     let config_specific_address = UpnpConfig {
50//!         address: Some(Ipv4Cidr::from_str("192.168.0.10/24")?),
51//!         port: 8080,
52//!         protocol: PortMappingProtocol::TCP,
53//!         duration: 3600,
54//!         comment: "Webserver alternative".to_string(),
55//!     };
56//!
57//!     let config_address_range = UpnpConfig {
58//!         address: Some(Ipv4Cidr::from_str("192.168.0")?),
59//!         port: 8081,
60//!         protocol: PortMappingProtocol::TCP,
61//!         duration: 3600,
62//!         comment: "Webserver second alternative".to_string(),
63//!     };
64//!
65//!     Ok([
66//!         config_no_address,
67//!         config_specific_address,
68//!         config_address_range,
69//!     ])
70//! }
71//!
72//! fn main() -> Result<(), Box<dyn Error>> {
73//!     for result in add_ports(get_configs()?) {
74//!         if let Err(err) = result {
75//!             error!("{}", err);
76//!         }
77//!     }
78//!
79//!     for result in delete_ports(get_configs()?) {
80//!         if let Err(err) = result {
81//!             error!("{}", err);
82//!         }
83//!     }
84//!
85//!     Ok(())
86//! }
87//! ```
88
89#![deny(missing_docs)]
90
91use std::net::{IpAddr, SocketAddr, SocketAddrV4};
92
93pub use cidr_utils::cidr::Ipv4Cidr;
94use igd_next::{Gateway, SearchOptions};
95use log::{debug, error, info, warn};
96use serde::Deserialize;
97use thiserror::Error;
98
99/// Convenience wrapper over all possible Errors
100#[allow(missing_docs)]
101#[derive(Debug, Error)]
102pub enum Error {
103    #[error("No matching gateway found")]
104    NoMatchingGateway,
105
106    #[error("Could not get interface address: {0}")]
107    CannotGetInterfaceAddress(#[source] std::io::Error),
108
109    #[error("Error adding port: {0}")]
110    IgdAddPortError(#[from] igd_next::AddPortError),
111
112    #[error("Error searching for gateway: {0}")]
113    IgdSearchError(#[from] igd_next::SearchError),
114}
115
116type Result<R> = std::result::Result<R, Error>;
117
118/// The protocol for which the given port will be opened. Possible values are
119/// [`UDP`](PortMappingProtocol::UDP) and [`TCP`](PortMappingProtocol::TCP).
120#[allow(missing_docs)]
121#[derive(Clone, Copy, Debug, Deserialize)]
122pub enum PortMappingProtocol {
123    TCP,
124    UDP,
125}
126
127impl From<PortMappingProtocol> for igd_next::PortMappingProtocol {
128    fn from(proto: PortMappingProtocol) -> Self {
129        match proto {
130            PortMappingProtocol::TCP => igd_next::PortMappingProtocol::TCP,
131            PortMappingProtocol::UDP => igd_next::PortMappingProtocol::UDP,
132        }
133    }
134}
135
136fn find_gateway_with_bind_addr(bind_addr: SocketAddr) -> Result<Gateway> {
137    let options = SearchOptions {
138        bind_addr,
139        ..Default::default()
140    };
141    Ok(igd_next::search_gateway(options)?)
142}
143
144fn find_gateway_and_addr(cidr: &Option<Ipv4Cidr>) -> Result<(Gateway, SocketAddr)> {
145    let ifaces = if_addrs::get_if_addrs().map_err(Error::CannotGetInterfaceAddress)?;
146
147    let (gateway, address) = ifaces
148        .iter()
149        .filter_map(|iface| {
150            if iface.is_loopback() || !iface.ip().is_ipv4() {
151                None
152            } else {
153                let iface_ip = match iface.ip() {
154                    IpAddr::V4(ip) => ip,
155                    IpAddr::V6(_) => unreachable!(),
156                };
157
158                match cidr {
159                    Some(cidr) if !cidr.contains(iface_ip) => None,
160                    Some(_) => {
161                        let addr = SocketAddr::new(IpAddr::V4(iface_ip), 0);
162
163                        let gateway = find_gateway_with_bind_addr(addr);
164
165                        Some((gateway, addr))
166                    }
167                    _ => {
168                        let options = SearchOptions {
169                            // Unwrap is okay here, IP is correctly generated
170                            bind_addr: format!("{}:0", iface.addr.ip()).parse().unwrap(),
171                            ..Default::default()
172                        };
173                        igd_next::search_gateway(options).ok().and_then(|gateway| {
174                            if let if_addrs::IfAddr::V4(addr) = &iface.addr {
175                                Some((Ok(gateway), SocketAddr::V4(SocketAddrV4::new(addr.ip, 0))))
176                            } else {
177                                // Anything other than V4 has been ruled out by the first if
178                                // condition.
179                                unreachable!()
180                            }
181                        })
182                    }
183                }
184            }
185        })
186        .next()
187        .ok_or_else(|| Error::NoMatchingGateway)?;
188
189    Ok((gateway?, address))
190}
191
192fn get_gateway_and_address_from_options(
193    address: &Option<Ipv4Cidr>,
194    port: u16,
195) -> Result<(Gateway, SocketAddr)> {
196    if let Some(addr) = address {
197        if addr.get_bits() == 32 {
198            let sock_addr = SocketAddr::new(IpAddr::V4(addr.get_prefix_as_ipv4_addr()), port);
199            let gateway = find_gateway_with_bind_addr(sock_addr)?;
200            return Ok((gateway, sock_addr));
201        }
202    }
203
204    let (gateway, mut addr) = find_gateway_and_addr(address)?;
205    addr.set_port(port);
206    Ok((gateway, addr))
207}
208
209/// This struct defines a configuration for a port mapping.
210///
211/// The configuration consists of all necessary pieces of information for a proper port opening.
212///
213/// # Examples
214///
215/// ```
216/// use easy_upnp::{Ipv4Cidr, PortMappingProtocol, UpnpConfig};
217///
218/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
219/// let config_no_address = UpnpConfig {
220///     address: None,
221///     port: 80,
222///     protocol: PortMappingProtocol::TCP,
223///     duration: 3600,
224///     comment: "Webserver".to_string(),
225/// };
226///
227/// let config_specific_address = UpnpConfig {
228///     address: Some(Ipv4Cidr::from_str("192.168.0.10/24")?),
229///     port: 80,
230///     protocol: PortMappingProtocol::TCP,
231///     duration: 3600,
232///     comment: "Webserver".to_string(),
233/// };
234///
235/// let config_address_range = UpnpConfig {
236///     address: Some(Ipv4Cidr::from_str("192.168.0")?),
237///     port: 80,
238///     protocol: PortMappingProtocol::TCP,
239///     duration: 3600,
240///     comment: "Webserver".to_string(),
241/// };
242/// #
243/// # Ok(())
244/// # }
245/// ```
246#[derive(Debug, Deserialize)]
247pub struct UpnpConfig {
248    /// The IP address for which the port mapping should be added.
249    ///
250    /// This field can be [None], in which case every connected interface will be tried, until one
251    /// gateway reports success. Useful if the IP address is dynamic and not consistent over
252    /// reboots.
253    ///
254    /// Fill in an IP address if you want to add a port mapping for a foreign device, or if you
255    /// know your machine's address and want to slightly speed up the process.
256    ///
257    /// For examples how to specify IP addresses, check the documentation of [Ipv4Cidr].
258    pub address: Option<Ipv4Cidr>,
259
260    /// The port number to open for the given IP address.
261    ///
262    /// Note that we are greedy at the moment, if a port mapping is already in place, it will be
263    /// deleted and re-added with the given IP address. This might be configurable in a future
264    /// release.
265    pub port: u16,
266
267    /// The protocol for which the given port will be opened. Possible values are
268    /// [`UDP`](PortMappingProtocol::UDP) and [`TCP`](PortMappingProtocol::TCP).
269    pub protocol: PortMappingProtocol,
270
271    /// The lease duration for the port mapping in seconds.
272    ///
273    /// Please note that some UPnP capable routers might choose to ignore this value, so do not
274    /// exclusively rely on this.
275    pub duration: u32,
276
277    /// A comment about the reason for the port mapping.
278    ///
279    /// Will be stored together with the mapping in the router.
280    pub comment: String,
281}
282
283impl UpnpConfig {
284    fn remove_port(&self) -> Result<()> {
285        let port = self.port;
286        let protocol = self.protocol.into();
287
288        let (gateway, _) = get_gateway_and_address_from_options(&self.address, port)?;
289
290        gateway.remove_port(protocol, port).unwrap_or_else(|e| {
291            warn!(
292                "The following, non-fatal error appeared while deleting port {}:",
293                port
294            );
295            warn!("{}", e);
296        });
297
298        Ok(())
299    }
300
301    fn add_port(&self) -> Result<()> {
302        let port = self.port;
303        let protocol = self.protocol.into();
304        let duration = self.duration;
305        let comment = &self.comment;
306
307        let (gateway, addr) = get_gateway_and_address_from_options(&self.address, port)?;
308
309        let f = || gateway.add_port(protocol, port, addr, duration, comment);
310        f().or_else(|e| match e {
311            igd_next::AddPortError::PortInUse => {
312                debug!("Port already in use. Delete mapping.");
313                gateway.remove_port(protocol, port).unwrap();
314                debug!("Retry port mapping.");
315                f()
316            }
317            e => Err(e),
318        })?;
319
320        Ok(())
321    }
322}
323
324/// Add port mappings.
325///
326/// This function takes an iterable of [UpnpConfig]s and opens all configures ports.
327///
328/// Errors are logged, but otherwise ignored. An error during opening a port will not stop the
329/// processing of the other ports.
330///
331/// # Example
332///
333/// ```no_run
334/// use log::error;
335/// use easy_upnp::{add_ports, PortMappingProtocol, UpnpConfig};
336///
337/// let config = UpnpConfig {
338///     address: None,
339///     port: 80,
340///     protocol: PortMappingProtocol::TCP,
341///     duration: 3600,
342///     comment: "Webserver".to_string(),
343/// };
344///
345/// for result in add_ports([config]) {
346///     if let Err(err) = result {
347///         error!("{}", err);
348///     }
349/// }
350/// ```
351pub fn add_ports(
352    configs: impl IntoIterator<Item = UpnpConfig>,
353) -> impl Iterator<Item = Result<()>> {
354    configs.into_iter().map(|config| {
355        info!("Add port: {:?}", config);
356        config.add_port()
357    })
358}
359
360/// Delete port mappings.
361///
362/// This function takes an iterable of [UpnpConfig]s and closes all configures ports.
363///
364/// Errors are logged, but otherwise ignored. An error during closing a port will not stop the
365/// processing of the other ports.
366///
367/// # Example
368///
369/// ```no_run
370/// use log::error;
371/// use easy_upnp::{delete_ports, PortMappingProtocol, UpnpConfig};
372///
373/// let config = UpnpConfig {
374///     address: None,
375///     port: 80,
376///     protocol: PortMappingProtocol::TCP,
377///     duration: 3600,
378///     comment: "Webserver".to_string(),
379/// };
380///
381/// for result in delete_ports([config]) {
382///     if let Err(err) = result {
383///         error!("{}", err);
384///     }
385/// }
386/// ```
387pub fn delete_ports(
388    configs: impl IntoIterator<Item = UpnpConfig>,
389) -> impl Iterator<Item = Result<()>> {
390    configs.into_iter().map(|config| {
391        info!("Remove port: {:?}", config);
392        config.remove_port()
393    })
394}