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}