1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
//! # 🦀 NAT
//! A library providing a pure Rust implementation of a client for both the NAT Port Mapping Protocol (NAT-PMP, [RFC 6886](https://www.rfc-editor.org/rfc/rfc6886)) and the Port Control Protocol (PCP, [RFC 6887](https://www.rfc-editor.org/rfc/rfc6887)).
//! This library is intended to feel like high level, idiomatic Rust, while still maintaining a strong focus on performance. It is asynchronous and uses the [tokio](https://tokio.rs) runtime to avoid blocking operations and to succinctly handle timeouts on UDP sockets.
//! ## Usage
//! ```rust,no_run
//! async {
//! use std::{net::{IpAddr, Ipv4Addr}, num::NonZeroU16};
//! use crab_nat::{InternetProtocol, PortMapping, PortMappingOptions};
//! // Attempt a port mapping request through PCP first and fallback to NAT-PMP.
//! let mapping = match PortMapping::new(
//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), /* Address of the PCP server, often a gateway or firewall */
//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 167)), /* Address of our client, as seen by the gateway. Only strictly necessary for PCP */
//! InternetProtocol::Tcp, /* Protocol to map */
//! NonZeroU16::new(8080).unwrap(), /* Internal port, cannot be zero */
//! PortMappingOptions::default(), /* Optional configuration values, including suggested external port and lifetimes */
//! )
//! .await
//! {
//! Ok(m) => m,
//! Err(e) => return eprintln!("Failed to map port: {e:?}"),
//! };
//!
//! // ...
//!
//! // Try to safely drop the mapping.
//! if let Err((e, m)) = mapping.try_drop().await {
//! eprintln!("Failed to drop mapping {}:{}->{}: {e:?}", m.gateway(), m.external_port(), m.internal_port());
//! } else {
//! println!("Successfully deleted the mapping...");
//! }
//! };
//! ```
use std::{net::IpAddr, num::NonZeroU16};
use num_enum::TryFromPrimitive;
pub mod natpmp;
pub mod pcp;
// The RFC for NAT-PMP states that connections SHOULD make up to 9 attempts, <https://www.rfc-editor.org/rfc/rfc6886#section-3.1> page 6.
// The RFC for PCP states that connections SHOULD make attempts without a limit, <https://www.rfc-editor.org/rfc/rfc6887#section-8.1.1> page 22.
// However, that would be largely impractical so we set a sane default of 3 retries after an initial timeout fails.
const SANE_MAX_REQUEST_RETRIES: usize = 3;
/// The required port for NAT-PMP and its successor, PCP.
pub const GATEWAY_PORT: u16 = 5351;
/// The RFC recommended lifetime for a port mapping, <https://www.rfc-editor.org/rfc/rfc6886#section-3.3> page 12.
pub const RECOMMENDED_MAPPING_LIFETIME_SECONDS: u32 = 7200;
/// 8-bit version field in the NAT-PMP and PCP headers.
#[derive(Debug, PartialEq, TryFromPrimitive)]
#[repr(u8)]
pub enum VersionCode {
/// NAT-PMP identifies its version with a `0` byte.
NatPmp = 0,
/// PCP identifies its version with a `2` byte.
/// The RFC explicitly states that PCP must use version `2` because non-compliant
/// devices were created that used `1` before the creation of PCP.
Pcp = 2,
}
/// Specifies the protocol to map a port for.
#[repr(u8)]
#[derive(Clone, Copy, Debug)]
pub enum InternetProtocol {
Udp = 1,
Tcp,
}
/// Specifies a port mapping protocol, as well as any protocol specific parameters.
#[derive(Clone, Copy, Debug)]
pub enum PortMappingType {
NatPmp,
Pcp {
client: IpAddr,
nonce: pcp::Nonce,
external_ip: IpAddr,
},
}
/// Configuration of the timing of UDP requests to the gateway.
#[derive(Clone, Copy, Debug)]
pub struct TimeoutConfig {
/// The initial timeout for the first request. In general, the timeout will be doubled on each successive retry.
pub initial_timeout: std::time::Duration,
/// The maximum number of retries to attempt before giving up.
/// Note that the first request is not considered a retry.
pub max_retries: usize,
/// The maximum timeout to use for a retry.
pub max_retry_timeout: Option<std::time::Duration>,
}
/// Optional configuration values for a port mapping request.
#[derive(Clone, Copy, Default)]
pub struct PortMappingOptions {
/// The external port to try to map. The server is not guaranteed to use this port.
pub external_port: Option<NonZeroU16>,
/// The lifetime of the port mapping in seconds. The server is not guaranteed to use this lifetime.
pub lifetime_seconds: Option<u32>,
/// The configuration of the timing of UDP requests made to the gateway.
pub timeout_config: Option<TimeoutConfig>,
}
/// A port mapping on the gateway. Should be renewed with `.try_renew()` and deleted from the gateway with `.try_drop()`.
#[derive(Clone, Debug)]
pub struct PortMapping {
/// The address of the gateway the mapping is registered with.
gateway: IpAddr,
/// The protocol the mapping is for.
protocol: InternetProtocol,
/// The internal/local port of the port mapping.
internal_port: NonZeroU16,
/// The external port of the port mapping.
external_port: NonZeroU16,
/// The lifetime of the port mapping in seconds.
lifetime_seconds: u32,
/// The datetime the port mapping is set to expire at, using this machine's clock.
expiration: std::time::Instant,
/// The gateway epoch time when the port mapping was created.
gateway_epoch_seconds: u32,
/// The type of mapping protocol used, as well as any protocol specific parameters.
mapping_type: PortMappingType,
/// The configuration of the timing of UDP requests made to the gateway.
pub timeout_config: TimeoutConfig,
}
impl PortMapping {
/// Attempts to map a port on the gateway using PCP first and falling back to NAT-PMP.
/// Will request to use the given external port if specified, otherwise it will let the gateway choose.
/// If no lifetime is specified, the NAT-PMP recommended lifetime of two hours will be used.
/// # Errors
/// Returns a `MappingFailure` enum which decomposes into `NatPmp(natpmp::Failure)` and `Pcp(pcp::Failure)` depending on which failed.
/// Will never return `Pcp(pcp::Failure::ResultCode(pcp::ResultCode::UnsupportedVersion))` because NAT-PMP will be used as a fallback in this case.
/// If a different `Pcp(_)` error is returned, then NAT-PMP is likely not supported by the gateway and this call will not attempt it.
/// If you want to still attempt NAT-PMP after PCP fails for unknown reasons, you can call `natpmp::try_port_mapping(..)` directly.
pub async fn new(
gateway: IpAddr,
client: IpAddr,
protocol: InternetProtocol,
internal_port: NonZeroU16,
mapping_options: PortMappingOptions,
) -> Result<PortMapping, MappingFailure> {
// Try to use the more modern protocol, PCP, first.
match pcp::try_port_mapping(
pcp::BaseMapRequest::new(gateway, client, protocol, internal_port),
None,
None,
mapping_options,
)
.await
{
// If we succeed, return the mapping.
Ok(m) => return Ok(m),
// If the gateway does not support PCP, fall back to NAT-PMP.
Err(pcp::Failure::ResultCode(pcp::ResultCode::UnsupportedVersion)) => {}
// Otherwise, return the error.
Err(e) => return Err(e.into()),
}
// Fall back to the older, possibly more widely supported, NAT-PMP.
natpmp::try_port_mapping(gateway, protocol, internal_port, mapping_options)
.await
.map_err(std::convert::Into::into)
}
/// Attempts to renew this port mapping on the gateway, otherwise returns an error.
/// # Errors
/// Returns a `MappingFailure` enum which decomposes into `NatPmp(natpmp::Failure)` and `Pcp(pcp::Failure)`
/// depending on which protocol was used to create the mapping.
pub async fn try_renew(&mut self) -> Result<(), MappingFailure> {
// The optional configuration values for the port mapping request.
let options = PortMappingOptions {
external_port: Some(self.external_port),
lifetime_seconds: Some(self.lifetime()),
timeout_config: Some(self.timeout_config),
};
// Attempt to renew the existing port mapping on the gateway.
match self.mapping_type {
PortMappingType::NatPmp => {
*self = natpmp::try_port_mapping(
self.gateway,
self.protocol,
self.internal_port,
options,
)
.await
.map_err(MappingFailure::from)?;
}
PortMappingType::Pcp {
client,
nonce,
external_ip,
} => {
*self = pcp::try_port_mapping(
pcp::BaseMapRequest::new(
self.gateway,
client,
self.protocol,
self.internal_port,
),
Some(nonce),
Some(external_ip),
options,
)
.await
.map_err(MappingFailure::from)?;
}
}
Ok(())
}
/// Attempts to safely delete this port mapping on the gateway, otherwise returns an error and the `PortMapping` back.
/// # Errors
/// Returns a `MappingFailure` enum which decomposes into `NatPmp(natpmp::Failure)` and `Pcp(pcp::Failure)`
/// depending on which protocol was used to create the mapping.
pub async fn try_drop(self) -> Result<(), (MappingFailure, Self)> {
let gateway = self.gateway();
let protocol = self.protocol();
let internal_port = self.internal_port();
let mapping_type = self.mapping_type();
// Attempt to delete the port mapping on the gateway.
match mapping_type {
PortMappingType::NatPmp => natpmp::try_drop_mapping(
self.gateway(),
self.protocol(),
Some(internal_port),
Some(self.timeout_config),
)
.await
.map_err(|e| (MappingFailure::from(e), self)),
PortMappingType::Pcp { client, nonce, .. } => pcp::try_drop_mapping(
gateway,
client,
nonce,
pcp::DropMappingRange::Single {
internal_port,
protocol,
},
Some(self.timeout_config),
)
.await
.map_err(|e| (MappingFailure::from(e), self)),
}
}
/// The address of the gateway the mapping is registered with.
#[must_use]
pub fn gateway(&self) -> IpAddr {
self.gateway
}
/// The protocol the mapping is for.
#[must_use]
pub fn protocol(&self) -> InternetProtocol {
self.protocol
}
/// The internal/local port of the port mapping.
#[must_use]
pub fn internal_port(&self) -> NonZeroU16 {
self.internal_port
}
/// The external port of the port mapping.
#[must_use]
pub fn external_port(&self) -> NonZeroU16 {
self.external_port
}
/// The lifetime of the port mapping in seconds.
#[must_use]
pub fn lifetime(&self) -> u32 {
self.lifetime_seconds
}
/// The datetime the port mapping is set to expire at, using this machine's clock.
#[must_use]
pub fn expiration(&self) -> std::time::Instant {
self.expiration
}
/// The gateway epoch time when the port mapping was created.
#[must_use]
pub fn gateway_epoch(&self) -> u32 {
self.gateway_epoch_seconds
}
/// The type of mapping protocol used, as well as any protocol specific parameters.
#[must_use]
pub fn mapping_type(&self) -> PortMappingType {
self.mapping_type
}
}
/// Private module for shared helper functions within the library.
mod helpers {
use std::time::Duration;
use tokio::net::UdpSocket;
use crate::TimeoutConfig;
/// Create a new UDP socket and connect it to the gateway socket address for NAT-PMP or PCP.
/// # Errors
/// Will return an error if we fail to bind to a local UDP socket or connect to the gateway address.
pub async fn new_socket(
gateway: std::net::IpAddr,
) -> Result<tokio::net::UdpSocket, std::io::Error> {
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
// Create a new UDP with an IP protocol matching that of the gateway address.
let socket = tokio::net::UdpSocket::bind(if gateway.is_ipv4() {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))
} else {
// The Rust standard library uses `0` as the `flowinfo` and `scope_id` for an `Ipv6Addr` created from an address and port number.
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0))
})
.await?;
socket.connect((gateway, crate::GATEWAY_PORT)).await?;
Ok(socket)
}
pub enum RequestSendError {
Socket(std::io::Error),
Timeout,
}
/// Send a request and wait for a response, retrying on timeout up to `max_retries` times.
/// Allow for a custom fuzzing function to be applied to the timeout after each retry. This is to avoid synchronization issues, but `std::convert::identity` can be used as a no-op.
/// # Errors
/// Will return a `Socket(..)` error if we:
/// * Failed to send data on the socket
/// * Failed to receive data on the socket
/// Otherwise, will return a `Timeout` error if the gateway could not be reached after all retries.
pub async fn try_send_until_response<F>(
timeout_config: TimeoutConfig,
socket: &UdpSocket,
send_bytes: &[u8],
recv_buf: &mut bytes::BytesMut,
fuzz_timeout: F,
) -> Result<usize, RequestSendError>
where
F: Fn(Duration) -> Duration,
{
// Create an internal helper to easily try sending and receiving packets, and springboard errors back to the caller.
async fn send_and_recv(
socket: &UdpSocket,
send_bytes: &[u8],
recv_buf: &mut bytes::BytesMut,
timeout: Duration,
) -> Result<usize, RequestSendError> {
socket
.send(send_bytes)
.await
.map_err(RequestSendError::Socket)?;
tokio::time::timeout(timeout, socket.recv_buf(recv_buf))
.await
.map_err(|_| RequestSendError::Timeout)?
.map_err(RequestSendError::Socket)
}
// Use the RFC recommended initial timeout and double it on each successive failure.
let mut wait = timeout_config.initial_timeout;
let mut retries = 0;
let max_retries = timeout_config.max_retries;
loop {
match send_and_recv(socket, send_bytes, recv_buf, wait).await {
// Return the number of bytes read from the response.
Ok(n) => return Ok(n),
// Retry on timeout up to `max_retries` times.
Err(RequestSendError::Timeout) => {
if retries >= max_retries {
return Err(RequestSendError::Timeout);
}
retries += 1;
// Both NAT-PMP and PCP have a base scaling of doubling the timeout each retry.
wait += wait;
// Limit the timeout to the configured maximum.
// This was added to in PCP RFC, but is supported here for both protocols.
if let Some(max) = timeout_config.max_retry_timeout {
if wait > max {
wait = max;
}
}
// PCP specifies that fuzzing be done after applying the maximum timeout, to avoid synchronization issues.
fuzz_timeout(wait);
// Optionally log retry attempts to tracing.
#[cfg(feature = "tracing")]
tracing::info!("Starting retry {retries}/{max_retries} with timeout {wait:?}");
}
// Any other error is returned immediately.
Err(e) => return Err(e),
}
}
}
}
/// Errors that occur during the respective port mapping protocols.
#[derive(Debug, thiserror::Error)]
pub enum MappingFailure {
#[error("NAT-PMP({0})")]
NatPmp(natpmp::Failure),
#[error("PCP({0})")]
Pcp(pcp::Failure),
}
impl From<natpmp::Failure> for MappingFailure {
fn from(f: natpmp::Failure) -> Self {
MappingFailure::NatPmp(f)
}
}
impl From<pcp::Failure> for MappingFailure {
fn from(f: pcp::Failure) -> Self {
MappingFailure::Pcp(f)
}
}