Skip to main content

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