1use std::net::{SocketAddr, ToSocketAddrs};
11use std::path::{Path, PathBuf};
12use tracing::{debug, info, warn};
13
14use crate::constants;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum NfsSecurity {
21 #[default]
23 Sys,
24 Krb5,
26 Krb5i,
28 Krb5p,
30}
31
32impl NfsSecurity {
33 pub fn requires_kerberos(self) -> bool {
35 !matches!(self, NfsSecurity::Sys)
36 }
37}
38
39impl std::fmt::Display for NfsSecurity {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 NfsSecurity::Sys => write!(f, "sys"),
43 NfsSecurity::Krb5 => write!(f, "krb5"),
44 NfsSecurity::Krb5i => write!(f, "krb5i"),
45 NfsSecurity::Krb5p => write!(f, "krb5p"),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum NfsTransportSecurity {
57 #[default]
59 None,
60 Tls,
63 Opportunistic,
66}
67
68#[derive(Debug, Clone)]
70pub struct NfsBypassInfo {
71 pub server_addr: SocketAddr,
73 pub server_hostname: String,
76 pub export_path: String,
78 pub mount_point: PathBuf,
80 pub v42_confirmed: bool,
82 pub security: NfsSecurity,
84 pub transport_security: NfsTransportSecurity,
87 pub rdma: bool,
91}
92
93impl Default for NfsBypassInfo {
94 fn default() -> Self {
95 Self {
96 server_addr: SocketAddr::from(([127, 0, 0, 1], 2049)),
97 server_hostname: String::new(),
98 export_path: String::new(),
99 mount_point: PathBuf::new(),
100 v42_confirmed: false,
101 security: NfsSecurity::default(),
102 transport_security: NfsTransportSecurity::default(),
103 rdma: false,
104 }
105 }
106}
107
108pub fn probe_nfs_bypass(path: &Path) -> Option<NfsBypassInfo> {
119 let canonical = path.canonicalize().ok()?;
120 let entries = crate::mount_info::list_mounts();
121
122 let best = entries
123 .iter()
124 .filter(|e| e.fs_type == "nfs" || e.fs_type == "nfs4")
125 .filter(|e| canonical.starts_with(&e.mount_point))
126 .max_by_key(|e| e.mount_point.len())?;
127
128 let mount_point = best.mount_point.clone();
129 let source = best.source.clone();
130 let super_opts = best.super_options.clone();
131
132 let security = super_opts
134 .split(',')
135 .find_map(|opt| match opt.trim() {
136 "sec=krb5p" => Some(NfsSecurity::Krb5p),
137 "sec=krb5i" => Some(NfsSecurity::Krb5i),
138 "sec=krb5" => Some(NfsSecurity::Krb5),
139 _ => None,
140 })
141 .unwrap_or(NfsSecurity::Sys);
142
143 let transport_security = super_opts
145 .split(',')
146 .find_map(|opt| match opt.trim() {
147 "xprtsec=tls" | "xprtsec=mtls" => Some(NfsTransportSecurity::Tls),
148 "xprtsec=none" => Some(NfsTransportSecurity::None),
149 _ => None,
150 })
151 .unwrap_or(NfsTransportSecurity::None);
152
153 let rdma = super_opts.split(',').any(|opt| opt.trim() == "proto=rdma");
154
155 #[cfg(not(feature = "tls"))]
156 if transport_security != NfsTransportSecurity::None {
157 info!(
158 "NFS bypass: xprtsec=tls detected on {} but tls feature is disabled -- \
159 using cleartext compound RPCs. Build with --features tls for RPC-over-TLS.",
160 mount_point
161 );
162 crate::metrics::NFS_BYPASS_TLS_FALLBACK.inc();
163 }
164
165 #[cfg(not(feature = "krb5"))]
167 if security.requires_kerberos() {
168 info!(
169 "NFS bypass unavailable for {}: Kerberos auth detected (sec={}) -- \
170 using kernel VFS path. Performance: large files unaffected; \
171 small-file throughput ~2.6x lower than AUTH_SYS bypass. \
172 Build with --features krb5 for RPCSEC_GSS support.",
173 mount_point, security
174 );
175 crate::metrics::NFS_BYPASS_KRB5_FALLBACK.inc();
176 return None;
177 }
178
179 #[cfg(feature = "krb5")]
181 if security.requires_kerberos() {
182 info!(
183 "NFS bypass: Kerberos auth detected (sec={}) for {} -- \
184 will use RPCSEC_GSS context for compound RPCs.",
185 security, mount_point
186 );
187 }
188
189 let v42 = super_opts.contains("vers=4.2") || super_opts.contains("nfsvers=4.2")
191 || super_opts.contains("vers=4,2") || super_opts.contains("minorversion=2");
192 if !v42 {
193 debug!("NFS bypass: mount {} is not NFSv4.2 (opts: {})", mount_point, super_opts);
194 return None;
195 }
196
197 let (server_str, export_path) = match source.split_once(':') {
199 Some((s, e)) => (s.to_string(), e.to_string()),
200 None => {
201 debug!("NFS bypass: cannot parse source '{}' for {}", source, mount_point);
202 return None;
203 }
204 };
205
206 let port: u16 = super_opts.split(',')
208 .find_map(|opt| {
209 let (k, v) = opt.split_once('=')?;
210 if k == "port" { v.parse().ok() } else { None }
211 })
212 .unwrap_or(constants::NFS_DEFAULT_PORT);
213
214 let addr_str = format!("{}:{}", server_str, port);
216 let server_addr = match addr_str.to_socket_addrs() {
217 Ok(mut addrs) => match addrs.next() {
218 Some(addr) => addr,
219 None => {
220 warn!("NFS bypass: no addresses resolved for {}", addr_str);
221 return None;
222 }
223 },
224 Err(e) => {
225 warn!("NFS bypass: cannot resolve {}: {}", addr_str, e);
226 return None;
227 }
228 };
229
230 debug!("NFS bypass: detected v4.2 mount {} -> {}:{} export={}",
231 mount_point, server_addr, port, export_path);
232
233 Some(NfsBypassInfo {
234 server_addr,
235 server_hostname: server_str,
236 export_path,
237 mount_point: PathBuf::from(mount_point),
238 v42_confirmed: v42,
239 security,
240 transport_security,
241 rdma,
242 })
243}
244
245pub fn get_mount_id(path: &Path) -> Option<u64> {
251 crate::mount_info::find_mount_for_path(path).map(|e| e.mount_id)
252}
253
254pub fn is_mount_present_global(path: &Path) -> bool {
262 let mounts = match std::fs::read_to_string("/proc/mounts") {
263 Ok(m) => m,
264 Err(_) => return true, };
266 let path_str = path.to_string_lossy();
269 for line in mounts.lines() {
272 let fields: Vec<&str> = line.split_whitespace().collect();
273 if fields.len() < 3 { continue; }
274 let mount_point = fields[1];
275 if mount_point == "/" { continue; }
276 if path_str.starts_with(mount_point)
277 && (path_str.len() == mount_point.len() || path_str.as_bytes().get(mount_point.len()) == Some(&b'/'))
278 {
279 return true;
280 }
281 }
282 false
283}
284
285const MAX_HANDLE_SZ: usize = 128;
287
288pub fn resolve_nfs_handle(dir_path: &Path) -> std::io::Result<Vec<u8>> {
295 use std::ffi::CString;
296 use std::os::unix::ffi::OsStrExt;
297
298 let path_cstr = CString::new(dir_path.as_os_str().as_bytes())
299 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
300
301 #[repr(C)]
303 struct FileHandleBuf {
304 handle_bytes: u32,
305 handle_type: i32,
306 f_handle: [u8; MAX_HANDLE_SZ],
307 }
308
309 let mut fh = FileHandleBuf {
310 handle_bytes: MAX_HANDLE_SZ as u32,
311 handle_type: 0,
312 f_handle: [0u8; MAX_HANDLE_SZ],
313 };
314 let mut mount_id: libc::c_int = 0;
315
316 let ret = unsafe {
319 libc::syscall(
320 libc::SYS_name_to_handle_at,
321 libc::AT_FDCWD,
322 path_cstr.as_ptr(),
323 &mut fh as *mut FileHandleBuf,
324 &mut mount_id as *mut libc::c_int,
325 0 as libc::c_int, )
327 };
328
329 if ret != 0 {
330 return Err(std::io::Error::last_os_error());
331 }
332
333 let handle_len = fh.handle_bytes as usize;
334 if handle_len > MAX_HANDLE_SZ {
335 return Err(std::io::Error::new(
336 std::io::ErrorKind::InvalidData,
337 format!("NFS handle too large: {} > {}", handle_len, MAX_HANDLE_SZ),
338 ));
339 }
340
341 let raw = &fh.f_handle[..handle_len];
342
343 if fh.handle_type >= 2 && handle_len > 14 {
354 let wire_region = &raw[14..];
355 let wire_len = wire_region.iter().rposition(|&b| b != 0)
357 .map(|p| p + 1)
358 .unwrap_or(0);
359 if wire_len > 0 {
360 return Ok(wire_region[..wire_len].to_vec());
361 }
362 }
363
364 Ok(raw.to_vec())
366}
367
368#[cfg(test)]
369mod tests {
370 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
371 use crate::constants;
372
373 #[test]
374 fn test_parse_nfs_source() {
375 let source = "server.example.com:/vol/data";
377 let (server, export) = source.split_once(':').unwrap();
378 assert_eq!(server, "server.example.com");
379 assert_eq!(export, "/vol/data");
380 }
381
382 #[test]
383 fn test_kerberos_detection() {
384 let opts = "rw,vers=4.2,sec=krb5p,rsize=1048576";
385 assert!(opts.contains("sec=krb5"));
386 }
387
388 #[test]
389 fn test_nfs_security_parse() {
390 use super::NfsSecurity;
391
392 let parse = |opts: &str| -> NfsSecurity {
393 opts.split(',')
394 .find_map(|opt| match opt.trim() {
395 "sec=krb5p" => Some(NfsSecurity::Krb5p),
396 "sec=krb5i" => Some(NfsSecurity::Krb5i),
397 "sec=krb5" => Some(NfsSecurity::Krb5),
398 _ => None,
399 })
400 .unwrap_or(NfsSecurity::Sys)
401 };
402
403 assert_eq!(parse("rw,vers=4.2,sec=krb5p,rsize=1048576"), NfsSecurity::Krb5p);
404 assert_eq!(parse("rw,vers=4.2,sec=krb5i"), NfsSecurity::Krb5i);
405 assert_eq!(parse("rw,vers=4.2,sec=krb5"), NfsSecurity::Krb5);
406 assert_eq!(parse("rw,vers=4.2,rsize=1048576"), NfsSecurity::Sys);
407 assert_eq!(parse("rw,vers=4.2,sec=sys"), NfsSecurity::Sys);
408 }
409
410 #[test]
411 fn test_nfs_security_requires_kerberos() {
412 use super::NfsSecurity;
413
414 assert!(!NfsSecurity::Sys.requires_kerberos());
415 assert!(NfsSecurity::Krb5.requires_kerberos());
416 assert!(NfsSecurity::Krb5i.requires_kerberos());
417 assert!(NfsSecurity::Krb5p.requires_kerberos());
418 }
419
420 #[test]
421 fn test_nfs_security_display() {
422 use super::NfsSecurity;
423
424 assert_eq!(format!("{}", NfsSecurity::Sys), "sys");
425 assert_eq!(format!("{}", NfsSecurity::Krb5), "krb5");
426 assert_eq!(format!("{}", NfsSecurity::Krb5i), "krb5i");
427 assert_eq!(format!("{}", NfsSecurity::Krb5p), "krb5p");
428 }
429
430 #[test]
431 fn test_nfs_security_default() {
432 use super::NfsSecurity;
433 assert_eq!(NfsSecurity::default(), NfsSecurity::Sys);
434 }
435
436 #[test]
437 fn test_v42_detection() {
438 assert!("vers=4.2,rsize=1048576".contains("vers=4.2"));
439 assert!("nfsvers=4.2".contains("nfsvers=4.2"));
440 assert!(!"vers=4.1,rsize=1048576".contains("vers=4.2"));
441 }
442
443 #[test]
444 fn test_port_extraction() {
445 let opts = "rw,vers=4.2,port=2050,rsize=1048576";
446 let port: u16 = opts.split(',')
447 .find_map(|opt| {
448 let (k, v) = opt.split_once('=')?;
449 if k == "port" { v.parse().ok() } else { None }
450 })
451 .unwrap_or(constants::NFS_DEFAULT_PORT);
452 assert_eq!(port, 2050);
453
454 let opts_no_port = "rw,vers=4.2,rsize=1048576";
455 let port2: u16 = opts_no_port.split(',')
456 .find_map(|opt| {
457 let (k, v) = opt.split_once('=')?;
458 if k == "port" { v.parse().ok() } else { None }
459 })
460 .unwrap_or(constants::NFS_DEFAULT_PORT);
461 assert_eq!(port2, constants::NFS_DEFAULT_PORT);
462 }
463
464 #[test]
465 fn transport_security_default_is_none() {
466 use super::NfsTransportSecurity;
467 assert_eq!(NfsTransportSecurity::default(), NfsTransportSecurity::None);
468 }
469
470 #[test]
471 fn transport_security_parse_tls() {
472 use super::NfsTransportSecurity;
473 let opts = "rw,sec=krb5,nfsvers=4.2,xprtsec=tls";
474 let result: Option<NfsTransportSecurity> = opts
475 .split(',')
476 .find_map(|opt| match opt.trim() {
477 "xprtsec=tls" | "xprtsec=mtls" => Some(NfsTransportSecurity::Tls),
478 "xprtsec=none" => Some(NfsTransportSecurity::None),
479 _ => None,
480 });
481 assert_eq!(result, Some(NfsTransportSecurity::Tls));
482 }
483
484 #[test]
485 fn transport_security_parse_mtls() {
486 use super::NfsTransportSecurity;
487 let opts = "rw,sec=krb5p,nfsvers=4.2,xprtsec=mtls";
488 let result: Option<NfsTransportSecurity> = opts
489 .split(',')
490 .find_map(|opt| match opt.trim() {
491 "xprtsec=tls" | "xprtsec=mtls" => Some(NfsTransportSecurity::Tls),
492 "xprtsec=none" => Some(NfsTransportSecurity::None),
493 _ => None,
494 });
495 assert_eq!(result, Some(NfsTransportSecurity::Tls));
496 }
497
498 #[test]
499 fn transport_security_absent_defaults_to_none() {
500 use super::NfsTransportSecurity;
501 let opts = "rw,sec=sys,nfsvers=4.2";
502 let result = opts
503 .split(',')
504 .find_map(|opt| match opt.trim() {
505 "xprtsec=tls" | "xprtsec=mtls" => Some(NfsTransportSecurity::Tls),
506 "xprtsec=none" => Some(NfsTransportSecurity::None),
507 _ => None,
508 })
509 .unwrap_or(NfsTransportSecurity::None);
510 assert_eq!(result, NfsTransportSecurity::None);
511 }
512
513 #[test]
514 fn detect_rdma_proto_in_mount_options() {
515 let opts = "rw,vers=4.2,proto=rdma,sec=krb5p,rsize=1048576";
516 let rdma = opts.split(',').any(|opt| opt.trim() == "proto=rdma");
517 assert!(rdma);
518 }
519
520 #[test]
521 fn detect_tcp_proto_as_not_rdma() {
522 let opts = "rw,vers=4.2,proto=tcp,sec=sys,rsize=1048576";
523 let rdma = opts.split(',').any(|opt| opt.trim() == "proto=rdma");
524 assert!(!rdma);
525
526 let opts_no_proto = "rw,vers=4.2,sec=sys";
527 let rdma2 = opts_no_proto.split(',').any(|opt| opt.trim() == "proto=rdma");
528 assert!(!rdma2);
529 }
530}