use std::net::SocketAddr;
use std::time::Duration;
use anyhow::Context as _;
use onc_rpc_client::transport::net::Connector as _;
use onc_rpc_client::transport::tokio::{TokioConnector, TokioIo};
use onc_rpcbind::{self as portmap, IPPROTO_TCP, PortmapperClient};
const PROG_NFS: u32 = 100_003;
const PROG_YPSERV: u32 = 100_004;
const PROG_YPBIND: u32 = 100_007;
#[derive(Debug, Clone, serde::Serialize)]
pub(crate) struct PortmapEntry {
pub program: u32,
pub version: u32,
pub protocol: u32,
pub port: u16,
}
#[derive(Debug, Clone)]
pub(crate) struct NisDetection {
pub ypserv_present: bool,
pub ypserv_port: Option<u16>,
pub ypbind_present: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct PortmapAmplificationResult {
pub request_bytes: usize,
pub response_bytes: usize,
pub factor: f64,
}
#[derive(Debug, Clone)]
pub(crate) struct PortmapClient {
port: u16,
proxy: Option<String>,
}
impl PortmapClient {
#[must_use]
pub(crate) const fn new(port: u16) -> Self {
Self { port, proxy: None }
}
#[must_use]
pub(crate) const fn default_port() -> Self {
Self::new(portmap::PMAP_PORT)
}
#[must_use]
pub(crate) fn with_proxy(mut self, proxy: String) -> Self {
self.proxy = Some(proxy);
self
}
async fn connect_tcp(&self, addr: SocketAddr) -> anyhow::Result<crate::proto::conn::NfsIo> {
if let Some(ref p) = self.proxy {
let proxy_addr = crate::proto::conn::parse_proxy_addr(p)?;
let stream = crate::proto::conn::socks5_connect(proxy_addr, addr).await.with_context(|| format!("SOCKS5 connect to {addr} via {p}"))?;
Ok(TokioIo::new(stream))
} else {
TokioConnector.connect(addr).await.with_context(|| format!("connect to {addr}"))
}
}
pub(crate) async fn query_port(&self, addr: SocketAddr, program: u32, version: u32) -> anyhow::Result<u16> {
let pmap_addr = SocketAddr::new(addr.ip(), self.port);
let io = self.connect_tcp(pmap_addr).await.with_context(|| format!("connect to portmapper at {pmap_addr}"))?;
let mut client = PortmapperClient::new(io);
client.getport(program, version, IPPROTO_TCP).await.with_context(|| format!("GETPORT {program}/{version}"))
}
pub(crate) async fn dump(&self, addr: SocketAddr) -> anyhow::Result<Vec<PortmapEntry>> {
let pmap_addr = SocketAddr::new(addr.ip(), self.port);
let io = self.connect_tcp(pmap_addr).await.with_context(|| format!("connect to portmapper at {pmap_addr}"))?;
let mut client = PortmapperClient::new(io);
let mappings = client.dump().await.context("PMAPPROC_DUMP")?;
Ok(mappings.into_iter().filter_map(|m| u16::try_from(m.port).ok().map(|port| PortmapEntry { program: m.prog, version: m.vers, protocol: m.prot, port })).collect())
}
pub(crate) async fn detect_nfs_versions(&self, addr: SocketAddr) -> anyhow::Result<Vec<u32>> {
let entries = self.dump(addr).await?;
let mut versions: Vec<u32> = entries.iter().filter(|e| e.program == PROG_NFS && e.protocol == IPPROTO_TCP).map(|e| e.version).collect();
versions.sort_unstable();
versions.dedup();
Ok(versions)
}
pub(crate) async fn detect_nis(&self, addr: SocketAddr) -> anyhow::Result<NisDetection> {
let entries = self.dump(addr).await?;
let ypserv = entries.iter().find(|e| e.program == PROG_YPSERV && e.protocol == IPPROTO_TCP);
let ypbind_present = entries.iter().any(|e| e.program == PROG_YPBIND);
Ok(NisDetection { ypserv_present: ypserv.is_some(), ypserv_port: ypserv.map(|e| e.port), ypbind_present })
}
pub(crate) async fn measure_amplification(&self, addr: SocketAddr) -> anyhow::Result<PortmapAmplificationResult> {
let request_bytes: usize = 64;
let entries = self.dump(addr).await?;
let response_bytes = entries.len().saturating_mul(20).saturating_add(8);
let factor = f64::from(u32::try_from(response_bytes).unwrap_or(u32::MAX)) / f64::from(u32::try_from(request_bytes).unwrap_or(1u32));
Ok(PortmapAmplificationResult { request_bytes, response_bytes, factor })
}
pub(crate) async fn dump_udp(&self, addr: SocketAddr, probe_timeout: Duration) -> anyhow::Result<Vec<PortmapEntry>> {
use onc_xdr::Void;
let pmap_addr = SocketAddr::new(addr.ip(), self.port);
let list: portmap::pmaplist = crate::proto::udp::call_rpc_udp(pmap_addr, portmap::PROGRAM, portmap::VERSION, 4, &Void, probe_timeout).await.context("PMAPPROC_DUMP over UDP")?;
Ok(list.0.into_iter().filter_map(|m| u16::try_from(m.port).ok().map(|port| PortmapEntry { program: m.prog, version: m.vers, protocol: m.prot, port })).collect())
}
pub(crate) async fn query_port_udp(&self, addr: SocketAddr, program: u32, version: u32, probe_timeout: Duration) -> anyhow::Result<u16> {
let pmap_addr = SocketAddr::new(addr.ip(), self.port);
let query = portmap::mapping { prog: program, vers: version, prot: IPPROTO_TCP, port: 0 };
let port: u32 = crate::proto::udp::call_rpc_udp(pmap_addr, portmap::PROGRAM, portmap::VERSION, 3, &query, probe_timeout).await.context("PMAPPROC_GETPORT over UDP")?;
u16::try_from(port).with_context(|| format!("port {port} out of u16 range"))
}
}