use clap::Parser;
use crate::cli::{H_PERMISSIONS, H_STEALTH, H_TARGET};
#[derive(Parser)]
pub(crate) struct MountArgs {
#[arg(help_heading = H_TARGET, value_name = "TARGET")]
pub target: String,
#[arg(help_heading = H_TARGET)]
pub mountpoint: String,
#[arg(short = 'e', long, group = "source", help_heading = H_TARGET)]
pub export: Option<String>,
#[arg(long, group = "source", help_heading = H_TARGET)]
pub handle: Option<String>,
#[arg(long, help_heading = H_PERMISSIONS)]
pub allow_write: bool,
#[arg(long, help_heading = H_STEALTH)]
pub hide: bool,
}
#[cfg(feature = "fuse")]
pub(crate) fn preflight(args: &MountArgs) -> anyhow::Result<()> {
if args.hide && args.handle.is_some() {
anyhow::bail!("--hide has no effect with --handle: there is no server-side mount to unmount");
}
match std::fs::metadata(&args.mountpoint) {
Ok(md) if md.is_dir() => Ok(()),
Ok(_) => anyhow::bail!("mountpoint {} is not a directory", args.mountpoint),
Err(e) => anyhow::bail!("mountpoint {} unusable: {e}", args.mountpoint),
}
}
#[cfg(feature = "fuse")]
pub(crate) async fn run(args: MountArgs, globals: &crate::cli::GlobalOpts) -> anyhow::Result<()> {
use std::net::{IpAddr, SocketAddr};
use std::path::Path;
use std::sync::Arc;
use fuser::MountOption;
use crate::cli::probe::make_mount_client;
use crate::proto::auth::{AuthSys, Credential};
use crate::proto::circuit::CircuitBreaker;
use crate::proto::conn::ReconnectStrategy;
use crate::proto::nfs3::Nfs3Client;
use crate::proto::nfs3::types::FileHandle;
use crate::proto::pool::{ConnectionPool, PoolKey};
use crate::proto::transport::PooledTransport;
use crate::util::stealth::StealthConfig;
tracing::info!(target = %args.target, mountpoint = %args.mountpoint, "mounting NFS export via FUSE");
let target = crate::cli::target::parse(&args.target, args.export.as_deref(), args.handle.as_deref(), true)?;
let host: IpAddr = target.host;
let (export, handle_hex) = match target.source {
crate::cli::target::Source::Export(p) => (p, None),
crate::cli::target::Source::Handle(h) => (String::from("/"), Some(h)),
crate::cli::target::Source::None => unreachable!("target::parse(.., true) rejected this"),
};
let export = export.as_str();
let addr = SocketAddr::new(host, 111);
let direct_nfs_port = match (handle_hex.is_some(), globals.nfs_port) {
(_, Some(p)) => Some(p),
(true, None) => Some(2049),
(false, None) => None,
};
let root_fh = if let Some(hex) = &handle_hex {
FileHandle::from_hex(hex)?
} else {
let mc = make_mount_client(globals);
eprintln!("{}", crate::output::status_info(&format!("Mounting {host}:{export}")));
let mr = mc.mount(addr, export).await?;
if args.hide {
match mc.unmount(addr, export).await {
Ok(()) => eprintln!("{}", crate::output::status_info("Stealth: unmounted from server")),
Err(e) => tracing::warn!(error = %e, "stealth UMNT failed; server may still show this client in its mount table"),
}
}
mr.handle
};
let pool = Arc::new(match &globals.proxy {
Some(p) => ConnectionPool::with_proxy(p.clone()),
None => ConnectionPool::default_config(),
});
let circuit = Arc::new(CircuitBreaker::default_config());
let gids = crate::cli::probe::build_gid_list(globals.gid, &globals.aux_gids);
let cred = Credential::Sys(AuthSys::with_groups(globals.uid, globals.gid, &gids, &globals.hostname));
let pool_key = PoolKey { host: addr, export: export.to_owned(), uid: globals.uid, gid: globals.gid };
let stealth = StealthConfig::new(globals.delay, globals.jitter);
let nfs3 = Arc::new(if let Some(nfs_port) = direct_nfs_port {
Nfs3Client::new(PooledTransport::new_direct(Arc::clone(&pool), pool_key, Arc::clone(&circuit), stealth, cred.clone(), ReconnectStrategy::Persistent, nfs_port))
} else {
Nfs3Client::new(PooledTransport::new(Arc::clone(&pool), pool_key, Arc::clone(&circuit), stealth, cred.clone(), ReconnectStrategy::Persistent))
});
let fs_name = if handle_hex.is_some() { format!("nfswolf:{host}:handle") } else { format!("nfswolf:{host}:{export}") };
let mut mount_options = vec![MountOption::FSName(fs_name), MountOption::DefaultPermissions, MountOption::Suid, MountOption::Dev];
if args.allow_write {
mount_options.push(MountOption::RW);
} else {
mount_options.push(MountOption::RO);
}
tracing::warn!("FUSE mount uses suid+dev passthrough for security testing (F-4.2, F-4.3) -- do not use on production systems");
let mut config = fuser::Config::default();
config.mount_options = mount_options;
config.acl = fuser::SessionACL::All;
let rt_handle = tokio::runtime::Handle::current();
let fs = crate::fuse::NfsFuse::new(crate::fuse::NfsFuseConfig { nfs3, root_fh, allow_write: args.allow_write, default_cred: cred, rt: rt_handle });
let mountpoint = Path::new(&args.mountpoint).to_path_buf();
tokio::task::spawn_blocking(move || fuser::mount(fs, &mountpoint, &config)).await??;
Ok(())
}