#![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 nfs_v3::MountClient;
use nfs_v3::wire::mount::dirpath;
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 tokio::net::TcpStream;
async fn start_memfs(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("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 portmap_client(port: u16) -> PortmapperClient<TokioIo<TcpStream>> {
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
let stream = TcpStream::connect(addr).await.expect("TCP connect must succeed");
PortmapperClient::new(TokioIo::new(stream))
}
#[tokio::test]
async fn memfs_export_has_wildcard_acl() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs(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 export at least one path");
assert!(!export_list.is_empty(), "MemFs must advertise at least one export");
}
#[tokio::test]
async fn memfs_advertises_auth_sys_no_kerberos() {
let mut config = MemFsConfig::default();
config.add_file("/test.txt", b"data".as_slice());
let (_server, port) = start_memfs(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 first_path = exports.into_inner().into_iter().next().map(|e| e.ex_dir.0.as_ref().to_vec()).expect("at least one export");
let mount_res = mc.v3_mnt(dirpath(Opaque::owned(first_path))).await.expect("MNT must succeed");
assert!(mount_res.auth_flavors.contains(&1), "MemFs must advertise AUTH_SYS (flavor 1)");
assert!(!mount_res.auth_flavors.contains(&6), "MemFs must NOT advertise RPCSEC_GSS (Kerberos)");
}
#[tokio::test]
async fn memfs_root_handle_is_non_empty() {
let mut config = MemFsConfig::default();
config.add_file("/dummy.txt", b"x".as_slice());
let (_server, port) = start_memfs(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 first_path = exports.into_inner().into_iter().next().map(|e| e.ex_dir.0.as_ref().to_vec()).expect("at least one export");
let mount_res = mc.v3_mnt(dirpath(Opaque::owned(first_path))).await.expect("MNT must succeed");
let fh = mount_res.fhandle.0.as_ref();
assert!(!fh.is_empty(), "root file handle must be non-empty");
assert!(fh.len() <= 128, "file handle must fit within NFS spec limits");
}
#[tokio::test]
async fn memfs_export_path_is_nonempty_string() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs(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();
for export in &export_list {
let path = export.ex_dir.0.as_ref();
assert!(!path.is_empty(), "export path must not be empty");
let path_str = std::str::from_utf8(path);
assert!(path_str.is_ok(), "export path must be valid UTF-8: {path:?}");
}
}
#[tokio::test]
async fn memfs_auth_flavors_are_valid() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs(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 first_path = exports.into_inner().into_iter().next().map(|e| e.ex_dir.0.as_ref().to_vec()).expect("at least one export");
let mount_res = mc.v3_mnt(dirpath(Opaque::owned(first_path))).await.expect("MNT must succeed");
let known_flavors: &[u32] = &[0, 1, 2, 6];
for &flavor in &mount_res.auth_flavors {
assert!(known_flavors.contains(&flavor) || flavor >= 300_000, "unexpected auth flavor {flavor} -- not a well-known value and not in RPCSEC_GSS range");
}
}
#[tokio::test]
async fn memfs_file_handle_usable_across_connections() {
use nfs_v3::Nfs3Client;
use nfs_v3::wire::{GETATTR3args, LOOKUP3args, Nfs3Result, diropargs3, filename3, nfs_fh3};
let mut config = MemFsConfig::default();
config.add_file("/bearer.txt", b"test data");
let (_server, port) = start_memfs(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mc = mount_client(port).await;
let mnt = mc.v3_mnt(dirpath(Opaque::borrowed(b"/"))).await.expect("MOUNT must succeed");
let root_fh = nfs_fh3 { data: mnt.fhandle.0.clone() };
let addr = std::net::SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), port);
let stream1 = TcpStream::connect(addr).await.expect("connect 1");
let nfs1 = Nfs3Client::new(DirectTransport::new(TokioIo::new(stream1)));
let fh = match nfs1.lookup(&LOOKUP3args { what: diropargs3 { dir: root_fh, name: filename3(Opaque::borrowed(b"bearer.txt")) } }).await.expect("LOOKUP must succeed") {
Nfs3Result::Ok(ok) => ok.object,
Nfs3Result::Err((stat, _)) => panic!("LOOKUP: {stat:?}"),
_ => unreachable!(),
};
let stream2 = TcpStream::connect(addr).await.expect("connect 2");
let nfs2 = Nfs3Client::new(DirectTransport::new(TokioIo::new(stream2)));
match nfs2.getattr(&GETATTR3args { object: fh }).await.expect("GETATTR must succeed") {
Nfs3Result::Ok(ok) => {
assert_eq!(ok.obj_attributes.type_, nfs_v3::wire::ftype3::NF3REG, "handle from conn 1 must work on conn 2");
},
Nfs3Result::Err((stat, _)) => panic!("GETATTR on second connection: {stat:?}"),
_ => unreachable!(),
}
}
#[tokio::test]
async fn memfs_portmapper_responds_to_nfs_getport() {
let config = MemFsConfig::default();
let (_server, port) = start_memfs(config).await;
tokio::time::sleep(Duration::from_millis(20)).await;
let mut pm = portmap_client(port).await;
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 v3 port matching server bind port");
}