diff --git a/iroh-net/src/portmapper.rs b/iroh-net/src/portmapper.rs index fac2e8a5a7..7573629a55 100644 --- a/iroh-net/src/portmapper.rs +++ b/iroh-net/src/portmapper.rs @@ -20,6 +20,7 @@ use current_mapping::CurrentMapping; mod current_mapping; mod mapping; mod metrics; +mod nat_pmp; mod pcp; mod upnp; @@ -38,20 +39,20 @@ const UNAVAILABILITY_TRUST_DURATION: Duration = Duration::from_secs(5); /// Output of a port mapping probe. #[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] -#[display("portmap={{ UPnP: {upnp}, PMP: {pmp}, PCP: {pcp} }}")] +#[display("portmap={{ UPnP: {upnp}, PMP: {nat_pmp}, PCP: {pcp} }}")] pub struct ProbeOutput { /// If UPnP can be considered available. pub upnp: bool, /// If PCP can be considered available. pub pcp: bool, /// If PMP can be considered available. - pub pmp: bool, + pub nat_pmp: bool, } impl ProbeOutput { /// Indicates if all port mapping protocols are available. pub fn all_available(&self) -> bool { - self.upnp && self.pcp && self.pmp + self.upnp && self.pcp && self.nat_pmp } } @@ -214,8 +215,8 @@ struct Probe { last_upnp_gateway_addr: Option<(upnp::Gateway, Instant)>, /// Last time PCP was seen. last_pcp: Option, - // TODO(@divma): PMP placeholder. - last_pmp: Option, + /// Last time NAT-PMP was seen. + last_nat_pmp: Option, } impl Default for Probe { @@ -224,7 +225,7 @@ impl Default for Probe { last_probe: Instant::now() - AVAILABILITY_TRUST_DURATION, last_upnp_gateway_addr: None, last_pcp: None, - last_pmp: None, + last_nat_pmp: None, } } } @@ -237,11 +238,11 @@ impl Probe { local_ip: Ipv4Addr, gateway: Ipv4Addr, ) -> Probe { - let ProbeOutput { upnp, pcp, pmp: _ } = output; + let ProbeOutput { upnp, pcp, nat_pmp } = output; let Config { enable_upnp, enable_pcp, - enable_nat_pmp: _, + enable_nat_pmp, } = config; let mut upnp_probing_task = util::MaybeFuture { inner: (enable_upnp && !upnp).then(|| { @@ -264,7 +265,15 @@ impl Probe { }), }; - let pmp_probing_task = async { None }; + let mut nat_pmp_probing_task = util::MaybeFuture { + inner: (enable_nat_pmp && !nat_pmp).then(|| { + Box::pin(async { + nat_pmp::probe_available(local_ip, gateway) + .await + .then(Instant::now) + }) + }), + }; if upnp_probing_task.inner.is_some() { inc!(Metrics, upnp_probes); @@ -272,23 +281,21 @@ impl Probe { let mut upnp_done = upnp_probing_task.inner.is_none(); let mut pcp_done = pcp_probing_task.inner.is_none(); - let mut pmp_done = true; - - tokio::pin!(pmp_probing_task); + let mut nat_pmp_done = nat_pmp_probing_task.inner.is_none(); let mut probe = Probe::default(); - while !upnp_done || !pcp_done || !pmp_done { + while !upnp_done || !pcp_done || !nat_pmp_done { tokio::select! { last_upnp_gateway_addr = &mut upnp_probing_task, if !upnp_done => { trace!("tick: upnp probe ready"); probe.last_upnp_gateway_addr = last_upnp_gateway_addr; upnp_done = true; }, - last_pmp = &mut pmp_probing_task, if !pmp_done => { - trace!("tick: pmp probe ready"); - probe.last_pmp = last_pmp; - pmp_done = true; + last_nat_pmp = &mut nat_pmp_probing_task, if !nat_pmp_done => { + trace!("tick: nat_pmp probe ready"); + probe.last_nat_pmp = last_nat_pmp; + nat_pmp_done = true; }, last_pcp = &mut pcp_probing_task, if !pcp_done => { trace!("tick: pcp probe ready"); @@ -318,10 +325,13 @@ impl Probe { .map(|last_probed| *last_probed + AVAILABILITY_TRUST_DURATION > now) .unwrap_or_default(); - // not probing for now - let pmp = false; + let nat_pmp = self + .last_nat_pmp + .as_ref() + .map(|last_probed| *last_probed + AVAILABILITY_TRUST_DURATION > now) + .unwrap_or_default(); - ProbeOutput { upnp, pcp, pmp } + ProbeOutput { upnp, pcp, nat_pmp } } /// Updates a probe with the `Some` values of another probe that is _assumed_ newer. @@ -330,7 +340,7 @@ impl Probe { last_probe, last_upnp_gateway_addr, last_pcp, - last_pmp, + last_nat_pmp, } = probe; if last_upnp_gateway_addr.is_some() { inc!(Metrics, upnp_available); @@ -359,8 +369,8 @@ impl Probe { inc!(Metrics, pcp_available); self.last_pcp = last_pcp; } - if last_pmp.is_some() { - self.last_pmp = last_pmp; + if last_nat_pmp.is_some() { + self.last_nat_pmp = last_nat_pmp; } self.last_probe = last_probe; @@ -571,7 +581,7 @@ impl Service { Err(e) => return debug!("can't get mapping: {e}"), }; - let ProbeOutput { upnp, pcp, pmp: _ } = self.full_probe.output(); + let ProbeOutput { upnp, pcp, nat_pmp } = self.full_probe.output(); debug!("getting a port mapping for {local_ip}:{local_port} -> {external_addr:?}"); let recently_probed = @@ -583,6 +593,10 @@ impl Service { self.mapping_task = if pcp || (!recently_probed && self.config.enable_pcp) { let task = mapping::Mapping::new_pcp(local_ip, local_port, gateway, external_addr); Some(tokio::spawn(task).into()) + } else if nat_pmp || (!recently_probed && self.config.enable_nat_pmp) { + let task = + mapping::Mapping::new_nat_pmp(local_ip, local_port, gateway, external_addr); + Some(tokio::spawn(task).into()) } else if upnp || self.config.enable_upnp { let external_port = external_addr.map(|(_addr, port)| port); let gateway = self diff --git a/iroh-net/src/portmapper/mapping.rs b/iroh-net/src/portmapper/mapping.rs index 8fd42c062a..89cf5e0d9f 100644 --- a/iroh-net/src/portmapper/mapping.rs +++ b/iroh-net/src/portmapper/mapping.rs @@ -4,7 +4,7 @@ use std::{net::Ipv4Addr, num::NonZeroU16, time::Duration}; use anyhow::Result; -use super::{pcp, upnp}; +use super::{nat_pmp, pcp, upnp}; pub(super) trait PortMapped: std::fmt::Debug + Unpin { fn external(&self) -> (Ipv4Addr, NonZeroU16); @@ -21,6 +21,9 @@ pub enum Mapping { /// A PCP mapping. #[debug(transparent)] Pcp(pcp::Mapping), + /// A NAT-PMP mapping. + #[debug(transparent)] + NatPmp(nat_pmp::Mapping), } impl Mapping { @@ -36,6 +39,23 @@ impl Mapping { .map(Self::Pcp) } + /// Create a new NAT-PMP mapping. + pub(crate) async fn new_nat_pmp( + local_ip: Ipv4Addr, + local_port: NonZeroU16, + gateway: Ipv4Addr, + external_addr: Option<(Ipv4Addr, NonZeroU16)>, + ) -> Result { + nat_pmp::Mapping::new( + local_ip, + local_port, + gateway, + external_addr.map(|(_addr, port)| port), + ) + .await + .map(Self::NatPmp) + } + /// Create a new UPnP mapping. pub(crate) async fn new_upnp( local_ip: Ipv4Addr, @@ -53,6 +73,7 @@ impl Mapping { match self { Mapping::Upnp(m) => m.release().await, Mapping::Pcp(m) => m.release().await, + Mapping::NatPmp(m) => m.release().await, } } } @@ -62,6 +83,7 @@ impl PortMapped for Mapping { match self { Mapping::Upnp(m) => m.external(), Mapping::Pcp(m) => m.external(), + Mapping::NatPmp(m) => m.external(), } } @@ -69,6 +91,7 @@ impl PortMapped for Mapping { match self { Mapping::Upnp(m) => m.half_lifetime(), Mapping::Pcp(m) => m.half_lifetime(), + Mapping::NatPmp(m) => m.half_lifetime(), } } } diff --git a/iroh-net/src/portmapper/nat_pmp.rs b/iroh-net/src/portmapper/nat_pmp.rs new file mode 100644 index 0000000000..bbed86ccbf --- /dev/null +++ b/iroh-net/src/portmapper/nat_pmp.rs @@ -0,0 +1,181 @@ +//! Definitions and utilities to interact with a NAT-PMP server. + +use std::{net::Ipv4Addr, num::NonZeroU16, time::Duration}; + +use tracing::{debug, trace}; + +use self::protocol::{MapProtocol, Request, Response}; + +mod protocol; + +/// Timeout to receive a response from a NAT-PMP server. +const RECV_TIMEOUT: Duration = Duration::from_millis(500); + +/// Recommended lifetime is 2 hours. See [RFC 6886 Requesting a +/// Mapping](https://datatracker.ietf.org/doc/html/rfc6886#section-3.3). +const MAPPING_REQUESTED_LIFETIME_SECONDS: u32 = 60 * 60 * 2; + +/// A mapping sucessfully registered with a NAT-PMP server. +#[derive(Debug)] +pub struct Mapping { + /// Local ip used to create this mapping. + local_ip: Ipv4Addr, + /// Local port used to create this mapping. + local_port: NonZeroU16, + /// Gateway address used to registed this mapping. + gateway: Ipv4Addr, + /// External port of the mapping. + external_port: NonZeroU16, + /// External address of the mapping. + external_addr: Ipv4Addr, + /// Allowed time for this mapping as informed by the server. + lifetime_seconds: u32, +} + +impl super::mapping::PortMapped for Mapping { + fn external(&self) -> (Ipv4Addr, NonZeroU16) { + (self.external_addr, self.external_port) + } + + fn half_lifetime(&self) -> Duration { + Duration::from_secs((self.lifetime_seconds / 2).into()) + } +} + +impl Mapping { + /// Attempt to register a new mapping with the NAT-PMP server on the provided gateway. + pub async fn new( + local_ip: Ipv4Addr, + local_port: NonZeroU16, + gateway: Ipv4Addr, + external_port: Option, + ) -> anyhow::Result { + // create the socket and send the request + let socket = tokio::net::UdpSocket::bind((local_ip, 0)).await?; + socket.connect((gateway, protocol::SERVER_PORT)).await?; + + let req = Request::Mapping { + proto: MapProtocol::Udp, + local_port: local_port.into(), + external_port: external_port.map(Into::into).unwrap_or_default(), + lifetime_seconds: MAPPING_REQUESTED_LIFETIME_SECONDS, + }; + + socket.send(&req.encode()).await?; + + // wait for the response and decode it + let mut buffer = vec![0; Response::MAX_SIZE]; + let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer)).await??; + let response = Response::decode(&buffer[..read])?; + + let (external_port, lifetime_seconds) = match response { + Response::PortMap { + proto: MapProtocol::Udp, + epoch_time: _, + private_port, + external_port, + lifetime_seconds, + } if private_port == Into::::into(local_port) => (external_port, lifetime_seconds), + _ => anyhow::bail!("server returned unexpected response for mapping request"), + }; + + let external_port = external_port + .try_into() + .map_err(|_| anyhow::anyhow!("received 0 port from server as external port"))?; + + // now send the second request to get the external address + let req = Request::ExternalAddress; + socket.send(&req.encode()).await?; + + // wait for the response and decode it + let mut buffer = vec![0; Response::MAX_SIZE]; + let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer)).await??; + let response = Response::decode(&buffer[..read])?; + + let external_addr = match response { + Response::PublicAddress { + epoch_time: _, + public_ip, + } => public_ip, + _ => anyhow::bail!("server returned unexpected response for mapping request"), + }; + + Ok(Mapping { + external_port, + external_addr, + lifetime_seconds, + local_ip, + local_port, + gateway, + }) + } + + /// Releases the mapping. + pub(crate) async fn release(self) -> anyhow::Result<()> { + // A client requests explicit deletion of a mapping by sending a message to the NAT gateway + // requesting the mapping, with the Requested Lifetime in Seconds set to zero. The + // Suggested External Port MUST be set to zero by the client on sending + + let Mapping { + local_ip, + local_port, + gateway, + .. + } = self; + + // create the socket and send the request + let socket = tokio::net::UdpSocket::bind((local_ip, 0)).await?; + socket.connect((gateway, protocol::SERVER_PORT)).await?; + + let req = Request::Mapping { + proto: MapProtocol::Udp, + local_port: local_port.into(), + external_port: 0, + lifetime_seconds: 0, + }; + + socket.send(&req.encode()).await?; + + // mapping deletion is a notification, no point in waiting for the response + Ok(()) + } +} + +/// Probes the local gateway for NAT-PMP support. +pub async fn probe_available(local_ip: Ipv4Addr, gateway: Ipv4Addr) -> bool { + match probe_available_fallible(local_ip, gateway).await { + Ok(response) => { + trace!("probe response: {response:?}"); + match response { + Response::PublicAddress { .. } => true, + _ => { + debug!("server returned an unexpected response type for probe"); + // missbehaving server is not useful + false + } + } + } + Err(e) => { + debug!("probe failed: {e}"); + false + } + } +} + +async fn probe_available_fallible( + local_ip: Ipv4Addr, + gateway: Ipv4Addr, +) -> anyhow::Result { + // create the socket and send the request + let socket = tokio::net::UdpSocket::bind((local_ip, 0)).await?; + socket.connect((gateway, protocol::SERVER_PORT)).await?; + let req = Request::ExternalAddress; + socket.send(&req.encode()).await?; + + // wait for the response and decode it + let mut buffer = vec![0; Response::MAX_SIZE]; + let read = tokio::time::timeout(RECV_TIMEOUT, socket.recv(&mut buffer)).await??; + let response = Response::decode(&buffer[..read])?; + + Ok(response) +} diff --git a/iroh-net/src/portmapper/nat_pmp/protocol.rs b/iroh-net/src/portmapper/nat_pmp/protocol.rs new file mode 100644 index 0000000000..08bea89464 --- /dev/null +++ b/iroh-net/src/portmapper/nat_pmp/protocol.rs @@ -0,0 +1,37 @@ +//! Definitions and utilities to interact with a NAT-PMP server. + +mod request; +mod response; + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +// PCP and NAT-PMP share same ports, reassigned by IANA from the older version to the new one. See +// + +pub use request::*; +pub use response::*; + +/// Port to use when acting as a server. This is the one we direct requests to. +pub const SERVER_PORT: u16 = 5351; + +/// Nat Version according to [RFC 6886 Transition to Port Control Protocol](https://datatracker.ietf.org/doc/html/rfc6886#section-1.1). +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum Version { + /// NAT-PMP version + NatPmp = 0, +} + +/// Opcode accepted by a NAT-PMP server. +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum Opcode { + /// Determine the external address of the gateway. + /// + /// See [RFC 6886 Determining the External Address](https://datatracker.ietf.org/doc/html/rfc6886#section-3.2). + DetermineExternalAddress = 0, + /// Get a UDP Mapping. + /// + /// See [RFC 6886 Requesting a Mapping](https://datatracker.ietf.org/doc/html/rfc6886#section-3.3). + MapUdp = 1, +} diff --git a/iroh-net/src/portmapper/nat_pmp/protocol/request.rs b/iroh-net/src/portmapper/nat_pmp/protocol/request.rs new file mode 100644 index 0000000000..42b2625bbb --- /dev/null +++ b/iroh-net/src/portmapper/nat_pmp/protocol/request.rs @@ -0,0 +1,129 @@ +//! A NAT-PCP request encoding and decoding. + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +use super::{Opcode, Version}; + +/// A NAT-PCP Request. +#[derive(Debug, PartialEq, Eq)] +pub enum Request { + /// Request to determine the gateway's external address. + ExternalAddress, + /// Request to register a mapping with the NAT-PCP server. + Mapping { + /// Protocol to use for this mapping. + proto: MapProtocol, + /// Local port to map. + local_port: u16, + /// Preferred external port. + external_port: u16, + /// Requested lifetime in seconds for the mapping. + lifetime_seconds: u32, + }, +} + +/// 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, +} + +impl Request { + /// Encode this [`Request`]. + pub fn encode(&self) -> Vec { + match self { + Request::ExternalAddress => vec![ + Version::NatPmp.into(), + Opcode::DetermineExternalAddress.into(), + ], + Request::Mapping { + proto, + local_port, + external_port, + lifetime_seconds, + } => { + let opcode = match proto { + MapProtocol::Udp => Opcode::MapUdp, + }; + let mut buf = vec![Version::NatPmp.into(), opcode.into()]; + buf.push(0); // reserved + buf.push(0); // reserved + buf.extend_from_slice(&local_port.to_be_bytes()); + buf.extend_from_slice(&external_port.to_be_bytes()); + buf.extend_from_slice(&lifetime_seconds.to_be_bytes()); + buf + } + } + } + + #[cfg(test)] + fn random(opcode: super::Opcode, rng: &mut R) -> Self { + match opcode { + Opcode::DetermineExternalAddress => Request::ExternalAddress, + Opcode::MapUdp => Request::Mapping { + proto: MapProtocol::Udp, + local_port: rng.gen(), + external_port: rng.gen(), + lifetime_seconds: rng.gen(), + }, + } + } + + #[cfg(test)] + #[track_caller] + fn decode(buf: &[u8]) -> Self { + let _version: Version = buf[0].try_into().unwrap(); + let opcode: super::Opcode = buf[1].try_into().unwrap(); + // 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, + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use rand::SeedableRng; + + #[test] + fn test_encode_decode_addr_request() { + let mut gen = rand_chacha::ChaCha8Rng::seed_from_u64(42); + + let request = Request::random(super::Opcode::DetermineExternalAddress, &mut gen); + let encoded = request.encode(); + assert_eq!(request, Request::decode(&encoded)); + } + + #[test] + fn test_encode_decode_map_request() { + let mut gen = rand_chacha::ChaCha8Rng::seed_from_u64(42); + + let request = Request::random(super::Opcode::MapUdp, &mut gen); + let encoded = request.encode(); + assert_eq!(request, Request::decode(&encoded)); + } +} diff --git a/iroh-net/src/portmapper/nat_pmp/protocol/response.rs b/iroh-net/src/portmapper/nat_pmp/protocol/response.rs new file mode 100644 index 0000000000..a7457ab887 --- /dev/null +++ b/iroh-net/src/portmapper/nat_pmp/protocol/response.rs @@ -0,0 +1,287 @@ +//! A NAT-PMP response encoding and decoding. + +use std::net::Ipv4Addr; + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +use super::{MapProtocol, Opcode, Version}; + +/// A NAT-PMP successful Response/Notification. +#[derive(Debug, PartialEq, Eq)] +pub enum Response { + /// Response to a [`Opcode::DetermineExternalAddress`] request. + PublicAddress { + epoch_time: u32, + public_ip: Ipv4Addr, + }, + /// Response to a [`Opcode::MapUdp`] request. + PortMap { + /// Protocol for which the mapping was requested. + proto: MapProtocol, + /// Epoch time of the server. + epoch_time: u32, + /// Local port for which the mapping was created. + private_port: u16, + /// External port registered for this mapping. + external_port: u16, + /// Lifetime in seconds that can be assumed by this mapping. + lifetime_seconds: u32, + }, +} + +/// Result code obtained in a NAT-PMP response. +/// +/// See [RFC 6886 Result Codes](https://datatracker.ietf.org/doc/html/rfc6886#section-3.5) +#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] +#[repr(u16)] +pub enum ResultCode { + /// A successful response. + Success = 0, + /// The sent version is not supported by the NAT-PMP server. + UnsupportedVersion = 1, + /// Functionality is suported but not allowerd: e.g. box supports mapping, but user has turned + /// feature off. + NotAuthorizedOrRefused = 2, + /// Netfork failures, e.g. NAT device itself has not obtained a DHCP lease. + NetworkFailure = 3, + /// NAT-PMP server cannot create any more mappings at this time. + OutOfResources = 4, + /// Opcode is not supported by the server. + UnsupportedOpcode = 5, +} + +/// Errors that can occur when decoding a [`Response`] from a server. +#[derive(Debug, derive_more::Display, thiserror::Error, PartialEq, Eq)] +pub enum Error { + /// Request is too short or is otherwise malformed. + #[display("Response is malformed")] + Malformed, + /// The [`Response::RESPONSE_INDICATOR`] is not present. + #[display("Packet does not appear to be a response")] + NotAResponse, + /// The received opcode is not recognized. + #[display("Invalid Opcode received")] + InvalidOpcode, + /// The received version is not recognized. + #[display("Invalid version received")] + InvalidVersion, + /// The received result code is not recognized. + #[display("Invalid result code received")] + InvalidResultCode, + /// Received an error code indicating the server does not support the sent version. + #[display("Server does not support the version")] + UnsupportedVersion, + /// Received an error code indicating the operation is supported but not authorized. + #[display("Operation is supported but not authorized")] + NotAuthorizedOrRefused, + /// Received an error code indicating the server experienced a network failure + #[display("Server experienced a network failure")] + NetworkFailure, + /// Received an error code indicating the server cannot create more mappings at this time. + #[display("Server is out of resources")] + OutOfResources, + /// Received an error code indicating the Opcode is not supported by the server. + #[display("Server does not suport this opcode")] + UnsupportedOpcode, +} + +impl Response { + /// Minimum size of an encoded [`Response`] sent by a server to this client. + pub const MIN_SIZE: usize = // parts of a public ip response + 1 + // version + 1 + // opcode + 2 + // result code + 4 + // epoch time + 4; // lifetime + + /// Minimum size of an encoded [`Response`] sent by a server to this client. + pub const MAX_SIZE: usize = // parts of mapping response + 1 + // version + 1 + // opcode + 2 + // result code + 4 + // epoch time + 2 + // private port + 2 + // public port + 4; // lifetime + + /// Indicator ORd into the [`Opcode`] to indicate a response packet. + pub const RESPONSE_INDICATOR: u8 = 1u8 << 7; + + /// Decode a response. + pub fn decode(buf: &[u8]) -> Result { + if buf.len() < Self::MIN_SIZE || buf.len() > Self::MAX_SIZE { + return Err(Error::Malformed); + } + let _: Version = buf[0].try_into().map_err(|_| Error::InvalidVersion)?; + let opcode = buf[1]; + if opcode & Self::RESPONSE_INDICATOR != Self::RESPONSE_INDICATOR { + return Err(Error::NotAResponse); + } + let opcode: Opcode = (opcode & !Self::RESPONSE_INDICATOR) + .try_into() + .map_err(|_| Error::InvalidOpcode)?; + + let result_bytes = + u16::from_be_bytes(buf[2..4].try_into().expect("slice has the right len")); + let result_code = result_bytes + .try_into() + .map_err(|_| Error::InvalidResultCode)?; + + match result_code { + ResultCode::Success => Ok(()), + ResultCode::UnsupportedVersion => Err(Error::UnsupportedVersion), + ResultCode::NotAuthorizedOrRefused => Err(Error::NotAuthorizedOrRefused), + ResultCode::NetworkFailure => Err(Error::NetworkFailure), + ResultCode::OutOfResources => Err(Error::OutOfResources), + ResultCode::UnsupportedOpcode => Err(Error::UnsupportedOpcode), + }?; + + let response = match opcode { + Opcode::DetermineExternalAddress => { + let epoch_bytes = buf[4..8].try_into().expect("slice has the right len"); + let epoch_time = u32::from_be_bytes(epoch_bytes); + let ip_bytes: [u8; 4] = buf[8..12].try_into().expect("slice has the right len"); + Response::PublicAddress { + epoch_time, + 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, + } + } + }; + + Ok(response) + } + + #[cfg(test)] + fn random(opcode: Opcode, rng: &mut R) -> Self { + match opcode { + Opcode::DetermineExternalAddress => { + let octects: [u8; 4] = rng.gen(); + Response::PublicAddress { + epoch_time: rng.gen(), + public_ip: octects.into(), + } + } + Opcode::MapUdp => Response::PortMap { + proto: MapProtocol::Udp, + epoch_time: rng.gen(), + private_port: rng.gen(), + external_port: rng.gen(), + lifetime_seconds: rng.gen(), + }, + } + } + + #[cfg(test)] + fn encode(&self) -> Vec { + match self { + Response::PublicAddress { + epoch_time, + public_ip, + } => { + let mut buf = Vec::with_capacity(Self::MIN_SIZE); + // version + buf.push(Version::NatPmp.into()); + // response indicator and opcode + let opcode: u8 = Opcode::DetermineExternalAddress.into(); + buf.push(Response::RESPONSE_INDICATOR | opcode); + // result code + let result_code: u16 = ResultCode::Success.into(); + for b in result_code.to_be_bytes() { + buf.push(b); + } + // epoch + for b in epoch_time.to_be_bytes() { + buf.push(b); + } + // public ip + for b in public_ip.octets() { + buf.push(b) + } + buf + } + Response::PortMap { + proto: _, + epoch_time, + private_port, + external_port, + lifetime_seconds, + } => { + let mut buf = Vec::with_capacity(Self::MAX_SIZE); + // version + buf.push(Version::NatPmp.into()); + // response indicator and opcode + let opcode: u8 = Opcode::MapUdp.into(); + buf.push(Response::RESPONSE_INDICATOR | opcode); + // result code + let result_code: u16 = ResultCode::Success.into(); + for b in result_code.to_be_bytes() { + buf.push(b); + } + // epoch + for b in epoch_time.to_be_bytes() { + buf.push(b); + } + // internal port + for b in private_port.to_be_bytes() { + buf.push(b) + } + // external port + for b in external_port.to_be_bytes() { + buf.push(b) + } + for b in lifetime_seconds.to_be_bytes() { + buf.push(b) + } + buf + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use rand::SeedableRng; + + #[test] + fn test_decode_external_addr_response() { + let mut gen = rand_chacha::ChaCha8Rng::seed_from_u64(42); + + let response = Response::random(Opcode::DetermineExternalAddress, &mut gen); + let encoded = response.encode(); + assert_eq!(Ok(response), Response::decode(&encoded)); + } + + #[test] + fn test_encode_decode_map_response() { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); + + let response = Response::random(Opcode::MapUdp, &mut rng); + let encoded = response.encode(); + assert_eq!(Ok(response), Response::decode(&encoded)); + } +} diff --git a/iroh-net/src/portmapper/pcp/protocol.rs b/iroh-net/src/portmapper/pcp/protocol.rs index 46844a233c..40d2fe02f8 100644 --- a/iroh-net/src/portmapper/pcp/protocol.rs +++ b/iroh-net/src/portmapper/pcp/protocol.rs @@ -10,7 +10,7 @@ pub use opcode_data::*; pub use request::*; pub use response::*; -// PCP and NAT-PMP share same ports, reasigned by IANA from the older version to the new one. See +// PCP and NAT-PMP share same ports, reassigned by IANA from the older version to the new one. See // /// Port to use when acting as a server. This is the one we direct requests to. diff --git a/iroh/src/commands/doctor.rs b/iroh/src/commands/doctor.rs index 3d516e46bd..5bef0fc249 100644 --- a/iroh/src/commands/doctor.rs +++ b/iroh/src/commands/doctor.rs @@ -122,10 +122,13 @@ pub enum Commands { /// Whether to enable PCP. #[clap(long)] enable_pcp: bool, + /// Whether to enable NAT-PMP. + #[clap(long)] + enable_nat_pmp: bool, }, /// Attempt to get a port mapping to the given local port. PortMap { - /// Protocol to use for port mapping. One of ["upnp", "pcp"]. + /// Protocol to use for port mapping. One of ["upnp", "nat_pmp", "pcp"]. protocol: String, /// Local port to get a mapping. local_port: NonZeroU16, @@ -612,9 +615,10 @@ async fn port_map(protocol: &str, local_port: NonZeroU16, timeout: Duration) -> // create the config that enables exlusively the required protocol let mut enable_upnp = false; let mut enable_pcp = false; - let enable_nat_pmp = false; + let mut enable_nat_pmp = false; match protocol.to_ascii_lowercase().as_ref() { "upnp" => enable_upnp = true, + "nat_pmp" => enable_nat_pmp = true, "pcp" => enable_pcp = true, other => anyhow::bail!("Unknown port mapping protocol {other}"), } @@ -835,11 +839,12 @@ pub async fn run(command: Commands, config: &Config) -> anyhow::Result<()> { Commands::PortMapProbe { enable_upnp, enable_pcp, + enable_nat_pmp, } => { let config = portmapper::Config { enable_upnp, enable_pcp, - enable_nat_pmp: false, + enable_nat_pmp, }; port_map_probe(config).await