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}