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::{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::AddPortError),
111
112    #[error("Error searching for gateway: {0}")]
113    IgdSearchError(#[from] igd::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::PortMappingProtocol {
128    fn from(proto: PortMappingProtocol) -> Self {
129        match proto {
130            PortMappingProtocol::TCP => igd::PortMappingProtocol::TCP,
131            PortMappingProtocol::UDP => igd::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::search_gateway(options)?)
142}
143
144fn find_gateway_and_addr(cidr: &Option<Ipv4Cidr>) -> Result<(Gateway, SocketAddr)> {
145    let ifaces = get_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::search_gateway(options).ok().and_then(|gateway| {
174                            if let get_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, SocketAddrV4)> {
196    Ok(match address {
197        Some(addr) if addr.get_bits() == 32 => {
198            let addr = SocketAddr::new(IpAddr::V4(addr.get_prefix_as_ipv4_addr()), port);
199
200            let gateway = find_gateway_with_bind_addr(addr)?;
201
202            let addr = match addr {
203                SocketAddr::V4(addr) => addr,
204                _ => panic!("No IPv4 given"),
205            };
206
207            (gateway, addr)
208        }
209
210        _ => {
211            let (gateway, mut addr) = find_gateway_and_addr(address)?;
212            addr.set_port(port);
213
214            let addr = match addr {
215                SocketAddr::V4(addr) => addr,
216                _ => panic!("No IPv4 given"),
217            };
218
219            (gateway, addr)
220        }
221    })
222}
223
224/// This struct defines a configuration for a port mapping.
225///
226/// The configuration consists of all necessary pieces of information for a proper port opening.
227///
228/// # Examples
229///
230/// ```
231/// use easy_upnp::{Ipv4Cidr, PortMappingProtocol, UpnpConfig};
232///
233/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
234/// let config_no_address = UpnpConfig {
235///     address: None,
236///     port: 80,
237///     protocol: PortMappingProtocol::TCP,
238///     duration: 3600,
239///     comment: "Webserver".to_string(),
240/// };
241///
242/// let config_specific_address = UpnpConfig {
243///     address: Some(Ipv4Cidr::from_str("192.168.0.10/24")?),
244///     port: 80,
245///     protocol: PortMappingProtocol::TCP,
246///     duration: 3600,
247///     comment: "Webserver".to_string(),
248/// };
249///
250/// let config_address_range = UpnpConfig {
251///     address: Some(Ipv4Cidr::from_str("192.168.0")?),
252///     port: 80,
253///     protocol: PortMappingProtocol::TCP,
254///     duration: 3600,
255///     comment: "Webserver".to_string(),
256/// };
257/// #
258/// # Ok(())
259/// # }
260/// ```
261#[derive(Debug, Deserialize)]
262pub struct UpnpConfig {
263    /// The IP address for which the port mapping should be added.
264    ///
265    /// This field can be [None], in which case every connected interface will be tried, until one
266    /// gateway reports success. Useful if the IP address is dynamic and not consistent over
267    /// reboots.
268    ///
269    /// Fill in an IP address if you want to add a port mapping for a foreign device, or if you
270    /// know your machine's address and want to slightly speed up the process.
271    ///
272    /// For examples how to specify IP addresses, check the documentation of [Ipv4Cidr].
273    pub address: Option<Ipv4Cidr>,
274
275    /// The port number to open for the given IP address.
276    ///
277    /// Note that we are greedy at the moment, if a port mapping is already in place, it will be
278    /// deleted and re-added with the given IP address. This might be configurable in a future
279    /// release.
280    pub port: u16,
281
282    /// The protocol for which the given port will be opened. Possible values are
283    /// [`UDP`](PortMappingProtocol::UDP) and [`TCP`](PortMappingProtocol::TCP).
284    pub protocol: PortMappingProtocol,
285
286    /// The lease duration for the port mapping in seconds.
287    ///
288    /// Please note that some UPnP capable routers might choose to ignore this value, so do not
289    /// exclusively rely on this.
290    pub duration: u32,
291
292    /// A comment about the reason for the port mapping.
293    ///
294    /// Will be stored together with the mapping in the router.
295    pub comment: String,
296}
297
298impl UpnpConfig {
299    fn remove_port(&self) -> Result<()> {
300        let port = self.port;
301        let protocol = self.protocol.into();
302
303        let (gateway, _) = get_gateway_and_address_from_options(&self.address, port)?;
304
305        gateway.remove_port(protocol, port).unwrap_or_else(|e| {
306            warn!(
307                "The following, non-fatal error appeared while deleting port {}:",
308                port
309            );
310            warn!("{}", e);
311        });
312
313        Ok(())
314    }
315
316    fn add_port(&self) -> Result<()> {
317        let port = self.port;
318        let protocol = self.protocol.into();
319        let duration = self.duration;
320        let comment = &self.comment;
321
322        let (gateway, addr) = get_gateway_and_address_from_options(&self.address, port)?;
323
324        let f = || gateway.add_port(protocol, port, addr, duration, comment);
325        f().or_else(|e| match e {
326            igd::AddPortError::PortInUse => {
327                debug!("Port already in use. Delete mapping.");
328                gateway.remove_port(protocol, port).unwrap();
329                debug!("Retry port mapping.");
330                f()
331            }
332            e => Err(e),
333        })?;
334
335        Ok(())
336    }
337}
338
339/// Add port mappings.
340///
341/// This function takes an iterable of [UpnpConfig]s and opens all configures ports.
342///
343/// Errors are logged, but otherwise ignored. An error during opening a port will not stop the
344/// processing of the other ports.
345///
346/// # Example
347///
348/// ```no_run
349/// use log::error;
350/// use easy_upnp::{add_ports, PortMappingProtocol, UpnpConfig};
351///
352/// let config = UpnpConfig {
353///     address: None,
354///     port: 80,
355///     protocol: PortMappingProtocol::TCP,
356///     duration: 3600,
357///     comment: "Webserver".to_string(),
358/// };
359///
360/// for result in add_ports([config]) {
361///     if let Err(err) = result {
362///         error!("{}", err);
363///     }
364/// }
365/// ```
366pub fn add_ports(
367    configs: impl IntoIterator<Item = UpnpConfig>,
368) -> impl Iterator<Item = Result<()>> {
369    configs.into_iter().map(|config| {
370        info!("Add port: {:?}", config);
371        config.add_port()
372    })
373}
374
375/// Delete port mappings.
376///
377/// This function takes an iterable of [UpnpConfig]s and closes all configures ports.
378///
379/// Errors are logged, but otherwise ignored. An error during closing a port will not stop the
380/// processing of the other ports.
381///
382/// # Example
383///
384/// ```no_run
385/// use log::error;
386/// use easy_upnp::{delete_ports, PortMappingProtocol, UpnpConfig};
387///
388/// let config = UpnpConfig {
389///     address: None,
390///     port: 80,
391///     protocol: PortMappingProtocol::TCP,
392///     duration: 3600,
393///     comment: "Webserver".to_string(),
394/// };
395///
396/// for result in delete_ports([config]) {
397///     if let Err(err) = result {
398///         error!("{}", err);
399///     }
400/// }
401/// ```
402pub fn delete_ports(
403    configs: impl IntoIterator<Item = UpnpConfig>,
404) -> impl Iterator<Item = Result<()>> {
405    configs.into_iter().map(|config| {
406        info!("Remove port: {:?}", config);
407        config.remove_port()
408    })
409}