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}