Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions portmapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ enum Message {
},
}

/// Configuration for UDP or TCP network protocol.
#[derive(Debug, Clone, Copy)]
pub enum Protocol {
/// UDP protocol.
Udp,
/// TCP protocol.
Tcp,
}

/// Configures which port mapping protocols are enabled in the [`Service`].
#[derive(Debug, Clone)]
pub struct Config {
Expand All @@ -117,15 +126,18 @@ pub struct Config {
pub enable_pcp: bool,
/// Whether PMP is enabled.
pub enable_nat_pmp: bool,
/// Whether to use UDP or TCP.
pub protocol: Protocol,
}

impl Default for Config {
/// By default all port mapping protocols are enabled.
/// By default all port mapping protocols are enabled for UDP.
fn default() -> Self {
Config {
enable_upnp: true,
enable_pcp: true,
enable_nat_pmp: true,
protocol: Protocol::Udp,
}
}
}
Expand Down Expand Up @@ -281,6 +293,7 @@ impl Probe {
enable_upnp,
enable_pcp,
enable_nat_pmp,
protocol: _,
} = config;
let mut upnp_probing_task = util::MaybeFuture {
inner: (enable_upnp && !upnp).then(|| {
Expand Down Expand Up @@ -633,21 +646,33 @@ impl Service {
debug!("getting a port mapping for {local_ip}:{local_port} -> {external_addr:?}");
let recently_probed =
self.full_probe.last_probe + UNAVAILABILITY_TRUST_DURATION > Instant::now();
let protocol = self.config.protocol;
// strategy:
// 1. check the available services and prefer pcp, then nat_pmp then upnp since it's
// the most unreliable, but possibly the most deployed one
// 2. if no service was available, fallback to upnp if enabled, followed by pcp and
// nat_pmp
self.mapping_task = if pcp {
// try pcp if available first
let task = mapping::Mapping::new_pcp(local_ip, local_port, gateway, external_addr);
let task = mapping::Mapping::new_pcp(
protocol,
local_ip,
local_port,
gateway,
external_addr,
);
Some(AbortOnDropHandle::new(tokio::spawn(
task.instrument(info_span!("pcp")),
)))
} else if nat_pmp {
// next nat_pmp if available
let task =
mapping::Mapping::new_nat_pmp(local_ip, local_port, gateway, external_addr);
let task = mapping::Mapping::new_nat_pmp(
protocol,
local_ip,
local_port,
gateway,
external_addr,
);
Some(AbortOnDropHandle::new(tokio::spawn(
task.instrument(info_span!("pmp")),
)))
Expand All @@ -659,23 +684,40 @@ impl Service {
.last_upnp_gateway_addr
.as_ref()
.map(|(gateway, _last_seen)| gateway.clone());
let task = mapping::Mapping::new_upnp(local_ip, local_port, gateway, external_port);
let task = mapping::Mapping::new_upnp(
protocol,
local_ip,
local_port,
gateway,
external_port,
);

Some(AbortOnDropHandle::new(tokio::spawn(
task.instrument(info_span!("upnp")),
)))
} else if !recently_probed && self.config.enable_pcp {
// if no service is available and the default fallback (upnp) is disabled, try pcp
// first
let task = mapping::Mapping::new_pcp(local_ip, local_port, gateway, external_addr);
let task = mapping::Mapping::new_pcp(
protocol,
local_ip,
local_port,
gateway,
external_addr,
);

Some(AbortOnDropHandle::new(tokio::spawn(
task.instrument(info_span!("pcp")),
)))
} else if !recently_probed && self.config.enable_nat_pmp {
// finally try nat_pmp if enabled
let task =
mapping::Mapping::new_nat_pmp(local_ip, local_port, gateway, external_addr);
let task = mapping::Mapping::new_nat_pmp(
protocol,
local_ip,
local_port,
gateway,
external_addr,
);
Some(AbortOnDropHandle::new(tokio::spawn(
task.instrument(info_span!("pmp")),
)))
Expand Down
9 changes: 7 additions & 2 deletions portmapper/src/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use nested_enum_utils::common_fields;
use snafu::{Backtrace, ResultExt, Snafu};

use super::{nat_pmp, pcp, upnp};
use crate::Protocol;

pub(super) trait PortMapped: std::fmt::Debug + Unpin {
fn external(&self) -> (Ipv4Addr, NonZeroU16);
Expand Down Expand Up @@ -43,25 +44,28 @@ pub enum Error {
impl Mapping {
/// Create a new PCP mapping.
pub(crate) async fn new_pcp(
protocol: Protocol,
local_ip: Ipv4Addr,
local_port: NonZeroU16,
gateway: Ipv4Addr,
external_addr: Option<(Ipv4Addr, NonZeroU16)>,
) -> Result<Self, Error> {
pcp::Mapping::new(local_ip, local_port, gateway, external_addr)
pcp::Mapping::new(protocol, local_ip, local_port, gateway, external_addr)
.await
.map(Self::Pcp)
.context(PcpSnafu)
}

/// Create a new NAT-PMP mapping.
pub(crate) async fn new_nat_pmp(
protocol: Protocol,
local_ip: Ipv4Addr,
local_port: NonZeroU16,
gateway: Ipv4Addr,
external_addr: Option<(Ipv4Addr, NonZeroU16)>,
) -> Result<Self, Error> {
nat_pmp::Mapping::new(
protocol,
local_ip,
local_port,
gateway,
Expand All @@ -74,12 +78,13 @@ impl Mapping {

/// Create a new UPnP mapping.
pub(crate) async fn new_upnp(
protocol: Protocol,
local_ip: Ipv4Addr,
local_port: NonZeroU16,
gateway: Option<upnp::Gateway>,
external_port: Option<NonZeroU16>,
) -> Result<Self, Error> {
upnp::Mapping::new(local_ip, local_port, gateway, external_port)
upnp::Mapping::new(protocol, local_ip, local_port, gateway, external_port)
.await
.map(Self::Upnp)
.context(UpnpSnafu)
Expand Down
15 changes: 11 additions & 4 deletions portmapper/src/nat_pmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use snafu::{Backtrace, Snafu};
use tracing::{debug, trace};

use self::protocol::{MapProtocol, Request, Response};
use crate::defaults::NAT_PMP_RECV_TIMEOUT as RECV_TIMEOUT;
use crate::{Protocol, defaults::NAT_PMP_RECV_TIMEOUT as RECV_TIMEOUT};

mod protocol;

Expand Down Expand Up @@ -63,6 +63,7 @@ impl super::mapping::PortMapped for Mapping {
impl Mapping {
/// Attempt to register a new mapping with the NAT-PMP server on the provided gateway.
pub async fn new(
protocol: Protocol,
local_ip: Ipv4Addr,
local_port: NonZeroU16,
gateway: Ipv4Addr,
Expand All @@ -72,8 +73,12 @@ impl Mapping {
let socket = UdpSocket::bind_full((local_ip, 0))?;
socket.connect((gateway, protocol::SERVER_PORT).into())?;

let proto = match protocol {
Protocol::Udp => MapProtocol::Udp,
Protocol::Tcp => MapProtocol::Tcp,
};
let req = Request::Mapping {
proto: MapProtocol::Udp,
proto,
local_port: local_port.into(),
external_port: external_port.map(Into::into).unwrap_or_default(),
lifetime_seconds: MAPPING_REQUESTED_LIFETIME_SECONDS,
Expand All @@ -92,12 +97,14 @@ impl Mapping {

let (external_port, lifetime_seconds) = match response {
Response::PortMap {
proto: MapProtocol::Udp,
proto: proto_rcvd,
epoch_time: _,
private_port,
external_port,
lifetime_seconds,
} if private_port == Into::<u16>::into(local_port) => (external_port, lifetime_seconds),
} if private_port == Into::<u16>::into(local_port) && proto == proto_rcvd => {
(external_port, lifetime_seconds)
}
_ => return Err(UnexpectedServerResponseSnafu.build()),
};

Expand Down
4 changes: 4 additions & 0 deletions portmapper/src/nat_pmp/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,8 @@ pub enum Opcode {
///
/// See [RFC 6886 Requesting a Mapping](https://datatracker.ietf.org/doc/html/rfc6886#section-3.3).
MapUdp = 1,
/// Get a TCP Mapping.
///
/// See [RFC 6886 Requesting a Mapping](https://datatracker.ietf.org/doc/html/rfc6886#section-3.3).
MapTcp = 2,
}
53 changes: 33 additions & 20 deletions portmapper/src/nat_pmp/protocol/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ pub enum Request {
}

/// Protocol for which a port mapping is requested.
// NOTE: spec defines TCP as well, which we don't need.
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum MapProtocol {
/// UDP mapping.
Udp = 1,
/// TCP mapping.
Tcp = 2,
}

impl Request {
Expand All @@ -47,6 +48,7 @@ impl Request {
} => {
let opcode = match proto {
MapProtocol::Udp => Opcode::MapUdp,
MapProtocol::Tcp => Opcode::MapTcp,
};
let mut buf = vec![Version::NatPmp.into(), opcode.into()];
buf.push(0); // reserved
Expand All @@ -69,6 +71,34 @@ impl Request {
external_port: rng.random(),
lifetime_seconds: rng.random(),
},
Opcode::MapTcp => Request::Mapping {
proto: MapProtocol::Tcp,
local_port: rng.random(),
external_port: rng.random(),
lifetime_seconds: rng.random(),
},
}
}

#[cfg(test)]
/// Decode a map request.
fn decode_map(buf: &[u8], proto: MapProtocol) -> Request {
// buf[2] reserved
// buf[3] reserved

let local_port_bytes = buf[4..6].try_into().expect("slice has the right size");
let local_port = u16::from_be_bytes(local_port_bytes);

let external_port_bytes = buf[6..8].try_into().expect("slice has the right size");
let external_port = u16::from_be_bytes(external_port_bytes);

let lifetime_bytes: [u8; 4] = buf[8..12].try_into().unwrap();
let lifetime_seconds = u32::from_be_bytes(lifetime_bytes);
Request::Mapping {
proto,
local_port,
external_port,
lifetime_seconds,
}
}

Expand All @@ -80,25 +110,8 @@ impl Request {
// check if this is a mapping request, or an external address request
match opcode {
Opcode::DetermineExternalAddress => Request::ExternalAddress,
Opcode::MapUdp => {
// buf[2] reserved
// buf[3] reserved

let local_port_bytes = buf[4..6].try_into().expect("slice has the right size");
let local_port = u16::from_be_bytes(local_port_bytes);

let external_port_bytes = buf[6..8].try_into().expect("slice has the right size");
let external_port = u16::from_be_bytes(external_port_bytes);

let lifetime_bytes: [u8; 4] = buf[8..12].try_into().unwrap();
let lifetime_seconds = u32::from_be_bytes(lifetime_bytes);
Request::Mapping {
proto: MapProtocol::Udp,
local_port,
external_port,
lifetime_seconds,
}
}
Opcode::MapUdp => Self::decode_map(buf, MapProtocol::Udp),
Opcode::MapTcp => Self::decode_map(buf, MapProtocol::Tcp),
}
}
}
Expand Down
55 changes: 32 additions & 23 deletions portmapper/src/nat_pmp/protocol/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,29 @@ impl Response {
/// Indicator ORd into the [`Opcode`] to indicate a response packet.
pub const RESPONSE_INDICATOR: u8 = 1u8 << 7;

/// Decode a map response.
fn decode_map(buf: &[u8], proto: MapProtocol) -> Response {
let epoch_bytes = buf[4..8].try_into().expect("slice has the right len");
let epoch_time = u32::from_be_bytes(epoch_bytes);

let private_port_bytes = buf[8..10].try_into().expect("slice has the right len");
let private_port = u16::from_be_bytes(private_port_bytes);

let external_port_bytes = buf[10..12].try_into().expect("slice has the right len");
let external_port = u16::from_be_bytes(external_port_bytes);

let lifetime_bytes = buf[12..16].try_into().expect("slice has the right len");
let lifetime_seconds = u32::from_be_bytes(lifetime_bytes);

Response::PortMap {
proto,
epoch_time,
private_port,
external_port,
lifetime_seconds,
}
}

/// Decode a response.
pub fn decode(buf: &[u8]) -> Result<Self, Error> {
if buf.len() < Self::MIN_SIZE || buf.len() > Self::MAX_SIZE {
Expand Down Expand Up @@ -153,29 +176,8 @@ impl Response {
public_ip: ip_bytes.into(),
}
}
Opcode::MapUdp => {
let proto = MapProtocol::Udp;

let epoch_bytes = buf[4..8].try_into().expect("slice has the right len");
let epoch_time = u32::from_be_bytes(epoch_bytes);

let private_port_bytes = buf[8..10].try_into().expect("slice has the right len");
let private_port = u16::from_be_bytes(private_port_bytes);

let external_port_bytes = buf[10..12].try_into().expect("slice has the right len");
let external_port = u16::from_be_bytes(external_port_bytes);

let lifetime_bytes = buf[12..16].try_into().expect("slice has the right len");
let lifetime_seconds = u32::from_be_bytes(lifetime_bytes);

Response::PortMap {
proto,
epoch_time,
private_port,
external_port,
lifetime_seconds,
}
}
Opcode::MapUdp => Self::decode_map(buf, MapProtocol::Udp),
Opcode::MapTcp => Self::decode_map(buf, MapProtocol::Tcp),
};

Ok(response)
Expand All @@ -198,6 +200,13 @@ impl Response {
external_port: rng.random(),
lifetime_seconds: rng.random(),
},
Opcode::MapTcp => Response::PortMap {
proto: MapProtocol::Tcp,
epoch_time: rng.random(),
private_port: rng.random(),
external_port: rng.random(),
lifetime_seconds: rng.random(),
},
}
}

Expand Down
Loading
Loading