#![allow(
unused_crate_dependencies,
unused_qualifications,
missing_docs,
missing_debug_implementations,
unused_import_braces,
unused_lifetimes,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
elided_lifetimes_in_paths,
explicit_outlives_requirements,
variant_size_differences,
clippy::all,
clippy::pedantic,
clippy::nursery,
clippy::cargo,
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::indexing_slicing,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
reason = "integration test -- all lints suppressed per project policy"
)]
use onc_rpc_client::transport::DirectTransport;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use assert_cmd::Command;
use nfs_v3::MountClient;
use nfs_v3::Nfs3Client;
use nfs_v3::wire::mount::dirpath;
use nfs_v3::wire::{GETATTR3args, LOOKUP3args, Nfs3Result, diropargs3, filename3};
use nfs3_server::memfs::{MemFs, MemFsConfig};
use nfs3_server::tcp::{NFSTcp, NFSTcpListener};
use onc_rpc_client::transport::tokio::TokioIo;
use onc_rpcbind::PortmapperClient;
use onc_xdr::Opaque;
use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use tokio::net::TcpStream;
#[test]
fn help_flag_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").arg("--help").assert().success().stdout(contains("NFS").or(contains("nfs")));
}
#[test]
fn scan_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["scan", "--help"]).assert().success();
}
#[test]
fn scan_auto_escape_flag_in_help() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["scan", "--help"]).assert().success().stdout(contains("--auto-escape"));
}
#[test]
fn analyze_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["analyze", "--help"]).assert().success();
}
#[test]
fn convert_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["convert", "--help"]).assert().success();
}
#[test]
fn shell_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["shell", "--help"]).assert().success();
}
#[test]
fn scan_missing_targets_fails() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").arg("scan").assert().failure();
}
#[test]
fn analyze_missing_target_fails() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").arg("analyze").assert().failure();
}
#[test]
fn completions_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["completions", "--help"]).assert().success();
}
#[cfg(feature = "fuse")]
#[test]
fn mount_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["mount", "--help"]).assert().success();
}
#[test]
fn escape_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["escape", "--help"]).assert().success();
}
#[test]
fn brute_handle_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["brute-handle", "--help"]).assert().success();
}
#[test]
fn uid_spray_help_succeeds() {
let _assert = Command::cargo_bin("nfswolf").expect("binary must be built").args(["uid-spray", "--help"]).assert().success();
}
#[tokio::test]
async fn memfs_export_list_shows_root() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let exports = mc.export().await.expect("MNTPROC_EXPORT must succeed");
let export_list = exports.into_inner();
assert!(!export_list.is_empty(), "MemFs must advertise at least one export");
let path = export_list[0].ex_dir.0.as_ref().to_vec();
let root_fh = mc.v3_mnt(dirpath(Opaque::owned(path))).await.expect("MNT must succeed");
assert!(!root_fh.fhandle.0.as_ref().is_empty(), "root handle must be non-empty");
}
#[tokio::test]
async fn memfs_portmapper_responds_to_getport() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
let stream = tokio::net::TcpStream::connect(addr).await.expect("connect must succeed");
let io = TokioIo::new(stream);
let mut pm = PortmapperClient::new(io);
let nfs_port = pm.getport(100_003, 3, onc_rpcbind::IPPROTO_TCP).await.expect("PMAPPROC_GETPORT for NFS v3 must succeed");
assert_eq!(nfs_port, port, "portmapper must report NFS port matching server bind port");
}
async fn start_memfs_server(config: MemFsConfig) -> (tokio::task::JoinHandle<()>, u16) {
let fs = MemFs::new(config).expect("MemFs creation must succeed");
let listener = NFSTcpListener::bind("127.0.0.1:0", fs).await.expect("NFSTcpListener::bind must succeed");
let port = listener.get_listen_port();
let handle = tokio::spawn(async move {
listener.handle_forever().await.expect("server must not crash");
});
(handle, port)
}
async fn mount_client(port: u16) -> MountClient<DirectTransport<TokioIo<TcpStream>>> {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
let stream = TcpStream::connect(addr).await.expect("TCP connect must succeed");
MountClient::v3(DirectTransport::new(TokioIo::new(stream)))
}
async fn nfs3_client(port: u16) -> Nfs3Client<DirectTransport<TokioIo<TcpStream>>> {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
let stream = TcpStream::connect(addr).await.expect("TCP connect must succeed");
Nfs3Client::new(DirectTransport::new(TokioIo::new(stream)))
}
#[tokio::test]
async fn memfs_mount_returns_root_handle() {
let mut config = MemFsConfig::default();
config.add_file("/hello.txt", b"hello world\n".as_slice());
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let mount_ok = mc.v3_mnt(dirpath(Opaque::borrowed(b"/"))).await.expect("MOUNT must succeed");
assert!(!mount_ok.fhandle.0.as_ref().is_empty(), "root handle must be non-empty");
assert!(!mount_ok.auth_flavors.is_empty(), "server must advertise at least one auth flavour");
}
#[tokio::test]
async fn memfs_getattr_on_root() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let mount_ok = mc.v3_mnt(dirpath(Opaque::borrowed(b"/"))).await.expect("MOUNT must succeed");
let root_fh = nfs_v3::wire::nfs_fh3 { data: mount_ok.fhandle.0.clone() };
let nfs = nfs3_client(port).await;
let result = nfs.getattr(&GETATTR3args { object: root_fh.clone() }).await.expect("GETATTR RPC must succeed");
match result {
Nfs3Result::Ok(ok) => {
assert_eq!(ok.obj_attributes.type_, nfs_v3::wire::ftype3::NF3DIR, "root must be a directory");
},
Nfs3Result::Err((stat, _)) => {
panic!("GETATTR failed: {stat:?}");
},
_ => unreachable!(),
}
}
#[tokio::test]
async fn memfs_lookup_file_in_root() {
let mut config = MemFsConfig::default();
config.add_file("/secret.txt", b"top secret\n".as_slice());
config.add_file("/other.txt", b"other\n".as_slice());
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let mount_ok = mc.v3_mnt(dirpath(Opaque::borrowed(b"/"))).await.expect("MOUNT must succeed");
let root_fh = nfs_v3::wire::nfs_fh3 { data: mount_ok.fhandle.0.clone() };
let nfs = nfs3_client(port).await;
let lookup_result = nfs.lookup(&LOOKUP3args { what: diropargs3 { dir: root_fh, name: filename3(Opaque::borrowed(b"secret.txt")) } }).await.expect("LOOKUP RPC must succeed");
match lookup_result {
Nfs3Result::Ok(ok) => {
assert!(!ok.object.data.as_ref().is_empty(), "file handle must be non-empty");
},
Nfs3Result::Err((stat, _)) => {
panic!("LOOKUP failed: {stat:?}");
},
_ => unreachable!(),
}
}
#[tokio::test]
async fn memfs_lookup_missing_file_returns_noent() {
use nfs_v3::wire::nfsstat3;
let config = MemFsConfig::default();
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let mount_ok = mc.v3_mnt(dirpath(Opaque::borrowed(b"/"))).await.expect("MOUNT must succeed");
let root_fh = nfs_v3::wire::nfs_fh3 { data: mount_ok.fhandle.0.clone() };
let nfs = nfs3_client(port).await;
let result = nfs.lookup(&LOOKUP3args { what: diropargs3 { dir: root_fh, name: filename3(Opaque::borrowed(b"does_not_exist.txt")) } }).await.expect("LOOKUP RPC must succeed (protocol level)");
match result {
Nfs3Result::Ok(_) => panic!("LOOKUP should have returned NOENT"),
Nfs3Result::Err((stat, _)) => {
assert_eq!(stat, nfsstat3::NFS3ERR_NOENT, "missing file must return NOENT");
},
_ => unreachable!(),
}
}
#[tokio::test]
async fn memfs_read_file_content() {
use nfs_v3::wire::{LOOKUP3args, READ3args, diropargs3};
let expected = b"integration test content";
let mut config = MemFsConfig::default();
config.add_file("/data.bin", expected.as_slice());
let (_server, port) = start_memfs_server(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let mount_ok = mc.v3_mnt(dirpath(Opaque::borrowed(b"/"))).await.expect("MOUNT must succeed");
let root_fh = nfs_v3::wire::nfs_fh3 { data: mount_ok.fhandle.0.clone() };
let nfs = nfs3_client(port).await;
let lookup_ok = match nfs.lookup(&LOOKUP3args { what: diropargs3 { dir: root_fh, name: filename3(Opaque::borrowed(b"data.bin")) } }).await.expect("LOOKUP must succeed") {
Nfs3Result::Ok(ok) => ok,
Nfs3Result::Err((stat, _)) => panic!("LOOKUP failed: {stat:?}"),
_ => unreachable!(),
};
let read_result = nfs.read(&READ3args { file: lookup_ok.object, offset: 0, count: 1024 }).await.expect("READ RPC must succeed");
match read_result {
Nfs3Result::Ok(ok) => {
assert_eq!(ok.data.as_ref(), expected, "read content must match written content");
assert!(ok.eof, "small file must be EOF on first read");
},
Nfs3Result::Err((stat, _)) => panic!("READ failed: {stat:?}"),
_ => unreachable!(),
}
}