use std::sync::Arc;
use std::time::Duration;
use nfs_v3::wire::ACCESS3args;
use tracing::{debug, warn};
use crate::proto::circuit::CircuitBreaker;
use crate::proto::nfs3::types::FileHandle;
use crate::proto::nfs3::{Nfs3Client, PooledNfs3 as _};
use crate::util::stealth::StealthConfig;
pub(crate) use crate::proto::nfs3::types::access as access_bits;
#[derive(Debug, Clone)]
pub(crate) struct SprayResult {
pub uid: u32,
pub gid: u32,
pub access: u32,
}
#[derive(Debug)]
pub(crate) struct SprayConfig {
pub uid_range: std::ops::RangeInclusive<u32>,
pub gid_range: std::ops::RangeInclusive<u32>,
pub paired_gid: bool,
pub auxiliary_gids: Vec<u32>,
pub _target_path: String,
pub _concurrency: usize,
pub required_access: u32,
pub per_attempt_delay_ms: u64,
}
const SPRAY_RPC_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) struct UidSprayer {
nfs3: Nfs3Client,
circuit: Arc<CircuitBreaker>,
stealth: StealthConfig,
}
impl std::fmt::Debug for UidSprayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UidSprayer").finish_non_exhaustive()
}
}
impl UidSprayer {
#[must_use]
pub(crate) const fn new(nfs3: Nfs3Client, circuit: Arc<CircuitBreaker>, stealth: StealthConfig) -> Self {
Self { nfs3, circuit, stealth }
}
pub(crate) async fn spray(&self, config: &SprayConfig, fh: &FileHandle) -> Vec<SprayResult> {
let mut results = Vec::new();
let nfs_fh = fh.to_nfs_fh3();
let args = ACCESS3args { object: nfs_fh, access: access_bits::ALL };
let mut conn = match self.nfs3.transport().checkout().await {
Ok(c) => c,
Err(e) => {
warn!(err = %e, "spray: failed to check out connection");
return results;
},
};
'outer: for uid in config.uid_range.clone() {
let gids: Vec<u32> = if config.paired_gid { vec![uid] } else { config.gid_range.clone().collect() };
for gid in gids {
if let Err(e) = self.check_circuit() {
warn!(?e, "circuit breaker open, stopping spray");
break 'outer;
}
if config.per_attempt_delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config.per_attempt_delay_ms)).await;
}
self.stealth.wait().await;
let call = conn.call_as::<_, nfs_v3::wire::ACCESS3res>(crate::proto::auth::AuthSys::with_groups(uid, gid, &config.auxiliary_gids, "nfswolf").to_opaque_auth(crate::proto::auth::next_stamp()), nfs_v3::PROGRAM, nfs_v3::VERSION, nfs_v3::wire::NFS_PROGRAM::NFSPROC3_ACCESS as u32, &args);
let Ok(outcome) = tokio::time::timeout(SPRAY_RPC_TIMEOUT, call).await else {
warn!(uid, gid, "spray: RPC timed out; abandoning the sweep");
conn.poison();
self.circuit.record_failure(self.nfs3.host());
break;
};
match outcome {
Ok(res) => match res {
nfs_v3::wire::Nfs3Result::Ok(ok) => {
let granted = ok.access;
debug!(uid, gid, access = granted, "spray: access granted");
if granted & config.required_access != 0 {
results.push(SprayResult { uid, gid, access: granted });
}
},
nfs_v3::wire::Nfs3Result::Err((stat, _)) => {
debug!(uid, gid, ?stat, "spray: access denied");
},
_ => {},
},
Err(e) => {
warn!(uid, gid, err = %e, "spray: RPC error");
conn.poison();
break 'outer;
},
}
}
}
drop(conn);
results
}
fn check_circuit(&self) -> anyhow::Result<()> {
self.circuit.check_or_wait(self.nfs3.host())
}
}