use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use anyhow::Context as _;
use crate::cli::GlobalOpts;
use crate::engine::credential::credential_ladder_with;
use crate::proto::auth::{AuthSys, Credential};
use crate::proto::circuit::CircuitBreaker;
use crate::proto::conn::ReconnectStrategy;
use crate::proto::mount::NfsMountClient;
use crate::proto::nfs2::Nfs2Client;
use crate::proto::nfs3::types::FileHandle;
use crate::proto::nfs3::{Nfs3Client, PooledNfs3 as _};
use crate::proto::pool::{ConnectionPool, PoolKey};
use crate::proto::transport::PooledTransport;
use crate::util::stealth::StealthConfig;
pub(crate) fn make_mount_client(globals: &GlobalOpts) -> NfsMountClient {
let mut base = globals.mount_port.map_or_else(NfsMountClient::new, NfsMountClient::with_port);
if let Some(ref p) = globals.proxy {
base = base.with_proxy(p.clone());
}
if globals.privileged_port { base.require_privileged() } else { base }
}
#[cfg(test)]
pub(crate) fn parse_addr(host: &str) -> anyhow::Result<SocketAddr> {
parse_addr_with_port(host, None)
}
pub(crate) fn parse_addr_with_port(host: &str, nfs_port: Option<u16>) -> anyhow::Result<SocketAddr> {
let port = nfs_port.unwrap_or(2049);
let host = host.find(":/").map_or(host, |idx| &host[..idx]);
if let Ok(addr) = host.parse::<SocketAddr>() {
return Ok(addr);
}
if let Ok(ip) = host.parse::<IpAddr>() {
return Ok(SocketAddr::new(ip, port));
}
if let Some(ip) = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).and_then(|inner| inner.parse::<IpAddr>().ok()) {
return Ok(SocketAddr::new(ip, port));
}
format!("{host}:{port}").parse::<SocketAddr>().with_context(|| format!("invalid host: {host}"))
}
fn make_pooled_transport(addr: SocketAddr, export: &str, uid: u32, gid: u32, aux_gids: &[u32], stealth: StealthConfig, proxy: Option<&str>, nfs_port: Option<u16>, hostname: &str) -> (Arc<ConnectionPool>, Arc<CircuitBreaker>, PooledTransport) {
let pool = Arc::new(match proxy {
Some(p) => ConnectionPool::with_proxy(p.to_owned()),
None => ConnectionPool::default_config(),
});
let circuit = Arc::new(CircuitBreaker::default_config());
let gids = build_gid_list(gid, aux_gids);
let auth = AuthSys::with_groups(uid, gid, &gids, hostname);
let cred = Credential::Sys(auth);
let key = PoolKey { host: addr, export: export.to_owned(), uid, gid };
let transport = match nfs_port {
Some(p) => PooledTransport::new_direct(Arc::clone(&pool), key, Arc::clone(&circuit), stealth, cred, ReconnectStrategy::Persistent, p),
None => PooledTransport::new(Arc::clone(&pool), key, Arc::clone(&circuit), stealth, cred, ReconnectStrategy::Persistent),
};
(pool, circuit, transport)
}
pub(crate) fn make_client_with_hostname(addr: SocketAddr, export: &str, uid: u32, gid: u32, aux_gids: &[u32], stealth: StealthConfig, proxy: Option<&str>, nfs_port: Option<u16>, hostname: &str) -> (Arc<ConnectionPool>, Arc<CircuitBreaker>, Nfs3Client) {
let (pool, circuit, transport) = make_pooled_transport(addr, export, uid, gid, aux_gids, stealth, proxy, nfs_port, hostname);
(pool, circuit, Nfs3Client::new(transport))
}
pub(crate) fn make_v2_client_with_hostname(addr: SocketAddr, export: &str, uid: u32, gid: u32, aux_gids: &[u32], stealth: StealthConfig, proxy: Option<&str>, nfs_port: Option<u16>, hostname: &str) -> (Arc<ConnectionPool>, Arc<CircuitBreaker>, Nfs2Client) {
let (pool, circuit, transport) = make_pooled_transport(addr, export, uid, gid, aux_gids, stealth, proxy, nfs_port, hostname);
(pool, circuit, Nfs2Client::new(transport))
}
pub(crate) fn build_gid_list(gid: u32, aux_gids: &[u32]) -> Vec<u32> {
let mut gids = vec![gid];
for &g in aux_gids {
if !gids.contains(&g) {
gids.push(g);
}
}
gids
}
pub(crate) async fn lookup_path(client: &Nfs3Client, root: &FileHandle, path: &str) -> anyhow::Result<FileHandle> {
let mut current = root.clone();
for component in path.split('/').filter(|s| !s.is_empty()) {
match client.resolve(¤t, component).await {
Ok((fh, _)) => current = fh,
Err(e) if e.is_permission_denied() => {
let facts = get_owner_uid(client, ¤t).await;
let try_uids = credential_ladder_with((client.uid(), client.gid()), facts.map(|f| f.0), facts.map(|f| f.1), &[]);
let mut resolved = false;
for (uid, gid) in &try_uids {
let esc_client = client.with_credential(Credential::Sys(AuthSys::with_groups(*uid, *gid, &[*gid], client.machinename())), *uid, *gid);
if let Ok((fh, _)) = esc_client.resolve(¤t, component).await {
tracing::debug!(component, uid, gid, "LOOKUP succeeded with escalated credential");
current = fh;
resolved = true;
break;
}
}
if !resolved {
anyhow::bail!("LOOKUP {component}: NFS3ERR_ACCES (tried {} credentials)", try_uids.len());
}
},
Err(e) => anyhow::bail!("LOOKUP {component}: {e}"),
}
}
Ok(current)
}
async fn get_owner_uid(client: &Nfs3Client, fh: &FileHandle) -> Option<((u32, u32), u32)> {
client.attrs(fh).await.ok().map(|a| ((a.uid, a.gid), a.mode))
}
use crate::engine::file_handle::{HandleVariant, dedup_variants, derive_handle_variants};
#[derive(Debug, Clone)]
pub(crate) struct TestedHandle {
pub variant: HandleVariant,
pub v3_ok: bool,
pub v3_stale: bool,
pub v2_ok: bool,
}
#[derive(Debug)]
pub(crate) struct HandleProbeResult {
pub tested: Vec<TestedHandle>,
pub auth_flavors: Vec<u32>,
pub v1_bypass: bool,
pub v3_error: Option<String>,
}
impl HandleProbeResult {
pub(crate) fn best_v3(&self) -> Option<&TestedHandle> {
self.tested.iter().find(|t| t.v3_ok)
}
pub(crate) fn escape_seeds(&self) -> Vec<&TestedHandle> {
self.tested.iter().filter(|t| t.v3_ok || t.v3_stale || t.v2_ok).collect()
}
}
pub(crate) async fn acquire_and_test_handles(mount: &NfsMountClient, _nfs3: &Nfs3Client, addr: SocketAddr, export: &str, stealth: &StealthConfig, nfs_port: Option<u16>, proxy: Option<&str>, hostname: &str) -> HandleProbeResult {
let direct_port = nfs_port.unwrap_or(2049);
let (_, _, probe_nfs3) = make_client_with_hostname(addr, export, 0, 0, &[], stealth.clone(), proxy, Some(direct_port), hostname);
stealth.wait().await;
let v3_result = mount.mount(addr, export).await;
stealth.wait().await;
let v1_result = mount.mount_v1(addr, export).await;
let v3_ok = v3_result.as_ref().ok();
let v1_ok = v1_result.as_ref().ok();
let v3_error = v3_result.as_ref().err().map(ToString::to_string);
let auth_flavors = v3_ok.map_or_else(|| v1_ok.map_or_else(Vec::new, |r| r.auth_flavors.clone()), |r| r.auth_flavors.clone());
let v1_bypass = v3_ok.is_none() && v1_ok.is_some();
let mut variants: Vec<HandleVariant> = Vec::new();
if let Some(r) = v3_ok {
variants.extend(derive_handle_variants(&r.handle, "v3"));
}
if let Some(r) = v1_ok {
variants.extend(derive_handle_variants(&r.handle, "v1"));
}
dedup_variants(&mut variants);
if variants.is_empty() {
return HandleProbeResult { tested: Vec::new(), auth_flavors, v1_bypass, v3_error };
}
let mut tested = Vec::with_capacity(variants.len());
for variant in variants {
stealth.wait().await;
let (v3_ok_flag, v3_stale_flag) = match probe_nfs3.attrs(&variant.handle).await {
Ok(_) => (true, false),
Err(e) if e.is_permission_denied() => (true, false),
Err(e) if e.is_stale() => (false, true),
_ => (false, false),
};
let v2_fh = nfs_v2::wire::Nfs2FileHandle::from_bytes(variant.handle.as_bytes());
let v2_handle = FileHandle::from_bytes(&v2_fh.0);
stealth.wait().await;
let v2_ok_flag = test_handle_v2(addr, &v2_handle, stealth, proxy, nfs_port, hostname).await;
tested.push(TestedHandle { variant, v3_ok: v3_ok_flag, v3_stale: v3_stale_flag, v2_ok: v2_ok_flag });
}
drop(mount.unmount(addr, export).await);
HandleProbeResult { tested, auth_flavors, v1_bypass, v3_error }
}
async fn test_handle_v2(addr: SocketAddr, handle: &FileHandle, stealth: &StealthConfig, proxy: Option<&str>, nfs_port: Option<u16>, hostname: &str) -> bool {
let (_, _, client) = make_v2_client_with_hostname(addr, "/", 0, 0, &[], stealth.clone(), proxy, nfs_port, hostname);
let v2_fh = nfs_v2::wire::Nfs2FileHandle::from_bytes(handle.as_bytes());
match client.getattr(&v2_fh).await {
Ok(_) => true,
Err(ref e) if format!("{e:?}").contains("Acces") || format!("{e:?}").contains("Perm") => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_addr_ipv4_default_port() {
assert_eq!(parse_addr("192.168.1.10").unwrap(), "192.168.1.10:2049".parse().unwrap());
}
#[test]
fn parse_addr_ipv4_with_export_strips_suffix() {
assert_eq!(parse_addr("192.168.1.10:/srv").unwrap(), "192.168.1.10:2049".parse().unwrap());
}
#[test]
fn parse_addr_ipv4_explicit_port_preserved() {
assert_eq!(parse_addr("192.168.1.10:12049").unwrap().port(), 12049);
}
#[test]
fn parse_addr_bare_ipv6_literal() {
assert_eq!(parse_addr("2001:db8::1").unwrap(), "[2001:db8::1]:2049".parse().unwrap());
}
#[test]
fn parse_addr_bracketed_ipv6_with_port() {
assert_eq!(parse_addr("[2001:db8::1]:2049").unwrap(), "[2001:db8::1]:2049".parse().unwrap());
}
#[test]
fn parse_addr_bracketed_ipv6_without_port() {
assert_eq!(parse_addr("[2001:db8::1]").unwrap(), "[2001:db8::1]:2049".parse().unwrap());
}
#[test]
fn parse_addr_ipv6_loopback_with_export() {
assert_eq!(parse_addr("[::1]:/export").unwrap(), "[::1]:2049".parse().unwrap());
}
#[test]
fn parse_addr_with_port_override_applies_to_ipv4_and_ipv6() {
assert_eq!(parse_addr_with_port("192.168.1.10", Some(20490)).unwrap().port(), 20490);
assert_eq!(parse_addr_with_port("2001:db8::1", Some(20490)).unwrap(), "[2001:db8::1]:20490".parse().unwrap());
}
#[test]
fn parse_addr_with_port_none_is_default() {
assert_eq!(parse_addr_with_port("192.168.1.10", None).unwrap().port(), 2049);
}
#[test]
fn parse_addr_invalid_host_errors() {
assert!(parse_addr("not a host").is_err());
}
#[test]
fn build_gid_list_puts_primary_first_and_dedups() {
assert_eq!(build_gid_list(42, &[42, 7, 7, 9]), vec![42, 7, 9]);
}
}