1#![allow(clippy::unwrap_used)]
13use std::io::{Read, Write};
14use std::net::TcpStream;
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
17use dashmap::DashMap;
18use tracing::{debug, info, warn};
19
20use crate::constants;
21
22use super::mount::NfsBypassInfo;
23use super::rpc::{self, Nfs4Op, StateId, WriteStable};
24use super::NfsError;
25
26#[cfg(not(any(feature = "tls", feature = "rdma")))]
34pub(crate) struct NfsTransport(TcpStream);
35
36#[cfg(not(any(feature = "tls", feature = "rdma")))]
37impl NfsTransport {
38 pub fn new(s: TcpStream) -> Self { Self(s) }
40 pub fn tcp_ref(&self) -> &TcpStream { &self.0 }
42 pub fn configure_tcp(&self) -> std::io::Result<()> {
44 use std::time::Duration;
45 self.0.set_nodelay(true)?;
46 self.0.set_read_timeout(Some(Duration::from_secs(constants::NFS_TCP_RW_TIMEOUT_SECS)))?;
47 self.0.set_write_timeout(Some(Duration::from_secs(constants::NFS_TCP_RW_TIMEOUT_SECS)))?;
48 Ok(())
49 }
50}
51
52#[cfg(not(any(feature = "tls", feature = "rdma")))]
53impl std::io::Read for NfsTransport {
54 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { self.0.read(buf) }
55}
56
57#[cfg(not(any(feature = "tls", feature = "rdma")))]
58impl std::io::Write for NfsTransport {
59 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.0.write(buf) }
60 fn flush(&mut self) -> std::io::Result<()> { self.0.flush() }
61}
62
63#[cfg(all(feature = "rdma", not(feature = "tls")))]
69pub(crate) enum NfsTransport {
70 Tcp(TcpStream),
72 Rdma(super::rdma_transport::RdmaConnection),
75}
76
77#[cfg(all(feature = "rdma", not(feature = "tls")))]
78impl NfsTransport {
79 pub fn new(s: TcpStream) -> Self { NfsTransport::Tcp(s) }
81 pub fn tcp_ref(&self) -> &TcpStream {
87 match self {
88 NfsTransport::Tcp(s) => s,
89 NfsTransport::Rdma(_) => unreachable!("RDMA transport has no TCP socket"),
90 }
91 }
92 pub fn configure_tcp(&self) -> std::io::Result<()> {
94 use std::time::Duration;
95 let tcp = self.tcp_ref();
96 tcp.set_nodelay(true)?;
97 tcp.set_read_timeout(Some(Duration::from_secs(constants::NFS_TCP_RW_TIMEOUT_SECS)))?;
98 tcp.set_write_timeout(Some(Duration::from_secs(constants::NFS_TCP_RW_TIMEOUT_SECS)))?;
99 Ok(())
100 }
101}
102
103#[cfg(all(feature = "rdma", not(feature = "tls")))]
104impl std::io::Read for NfsTransport {
105 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
106 match self {
107 NfsTransport::Tcp(s) => s.read(buf),
108 NfsTransport::Rdma(_) => unreachable!("RDMA uses rpc_call() directly, not Read"),
109 }
110 }
111}
112
113#[cfg(all(feature = "rdma", not(feature = "tls")))]
114impl std::io::Write for NfsTransport {
115 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
116 match self {
117 NfsTransport::Tcp(s) => s.write(buf),
118 NfsTransport::Rdma(_) => unreachable!("RDMA uses rpc_call() directly, not Write"),
119 }
120 }
121 fn flush(&mut self) -> std::io::Result<()> {
122 match self {
123 NfsTransport::Tcp(s) => s.flush(),
124 NfsTransport::Rdma(_) => unreachable!("RDMA uses rpc_call() directly, not Write"),
125 }
126 }
127}
128
129#[cfg(feature = "tls")]
130pub(crate) enum NfsTransport {
131 Tcp(TcpStream),
133 Tls(Box<rustls::StreamOwned<rustls::ClientConnection, TcpStream>>),
135 #[cfg(feature = "ktls")]
140 Ktls(TcpStream),
141 #[cfg(feature = "rdma")]
144 Rdma(super::rdma_transport::RdmaConnection),
145}
146
147#[cfg(feature = "tls")]
148impl NfsTransport {
149 pub fn new(s: TcpStream) -> Self { NfsTransport::Tcp(s) }
151 pub fn tcp_ref(&self) -> &TcpStream {
153 match self {
154 NfsTransport::Tcp(s) => s,
155 NfsTransport::Tls(s) => s.get_ref(),
156 #[cfg(feature = "ktls")]
157 NfsTransport::Ktls(s) => s,
158 #[cfg(feature = "rdma")]
159 NfsTransport::Rdma(_) => unreachable!("RDMA transport has no TCP socket"),
160 }
161 }
162 pub fn configure_tcp(&self) -> std::io::Result<()> {
164 use std::time::Duration;
165 let tcp = self.tcp_ref();
166 tcp.set_nodelay(true)?;
167 tcp.set_read_timeout(Some(Duration::from_secs(constants::NFS_TCP_RW_TIMEOUT_SECS)))?;
168 tcp.set_write_timeout(Some(Duration::from_secs(constants::NFS_TCP_RW_TIMEOUT_SECS)))?;
169 Ok(())
170 }
171}
172
173#[cfg(feature = "tls")]
174impl std::io::Read for NfsTransport {
175 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
176 match self {
177 NfsTransport::Tcp(s) => s.read(buf),
178 NfsTransport::Tls(s) => s.read(buf),
179 #[cfg(feature = "ktls")]
180 NfsTransport::Ktls(s) => s.read(buf),
181 #[cfg(feature = "rdma")]
182 NfsTransport::Rdma(_) => unreachable!("RDMA uses rpc_call() directly"),
183 }
184 }
185}
186
187#[cfg(feature = "tls")]
188impl std::io::Write for NfsTransport {
189 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
190 match self {
191 NfsTransport::Tcp(s) => s.write(buf),
192 NfsTransport::Tls(s) => s.write(buf),
193 #[cfg(feature = "ktls")]
194 NfsTransport::Ktls(s) => s.write(buf),
195 #[cfg(feature = "rdma")]
196 NfsTransport::Rdma(_) => unreachable!("RDMA uses rpc_call() directly"),
197 }
198 }
199 fn flush(&mut self) -> std::io::Result<()> {
200 match self {
201 NfsTransport::Tcp(s) => s.flush(),
202 NfsTransport::Tls(s) => s.flush(),
203 #[cfg(feature = "ktls")]
204 NfsTransport::Ktls(s) => s.flush(),
205 #[cfg(feature = "rdma")]
206 NfsTransport::Rdma(_) => unreachable!("RDMA uses rpc_call() directly"),
207 }
208 }
209}
210
211#[cfg(target_os = "linux")]
212impl NfsTransport {
213 #[allow(dead_code)]
215 pub fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
216 use std::os::unix::io::AsRawFd;
217 self.tcp_ref().as_raw_fd()
218 }
219}
220
221#[cfg(feature = "tls")]
222impl NfsTransport {
223 pub fn export_channel_binding(&self) -> Option<Vec<u8>> {
228 match self {
229 NfsTransport::Tls(tls) => {
230 let mut ekm = [0u8; 32];
231 match tls.conn.export_keying_material(&mut ekm, b"EXPORTER-Channel-Binding", None) {
232 Ok(_) => Some(ekm.to_vec()),
233 Err(e) => {
234 tracing::warn!("TLS EKM export failed (channel binding unavailable): {e}");
235 None
236 }
237 }
238 }
239 NfsTransport::Tcp(_) => None,
240 #[cfg(feature = "ktls")]
241 NfsTransport::Ktls(_) => None,
242 #[cfg(feature = "rdma")]
243 NfsTransport::Rdma(_) => None,
244 }
245 }
246}
247
248struct CompoundSizeTuner {
254 current_max: AtomicUsize,
256 negotiated_ceiling: usize,
258 consecutive_success: AtomicU32,
260}
261
262impl CompoundSizeTuner {
263 fn new(negotiated_max_ops: u32) -> Self {
264 let ceiling = ((negotiated_max_ops.saturating_sub(2)) / 3).max(1) as usize;
266 Self {
268 current_max: AtomicUsize::new(ceiling.min(3)),
269 negotiated_ceiling: ceiling,
270 consecutive_success: AtomicU32::new(0),
271 }
272 }
273
274 fn current(&self) -> usize {
276 self.current_max.load(Ordering::Relaxed)
277 }
278
279 fn on_success(&self) {
281 let succ = self.consecutive_success.fetch_add(1, Ordering::Relaxed) + 1;
282 if succ >= 10 {
283 let cur = self.current_max.load(Ordering::Relaxed);
284 if cur < self.negotiated_ceiling {
285 self.current_max.store(cur + 1, Ordering::Relaxed);
286 self.consecutive_success.store(0, Ordering::Relaxed);
287 debug!("CompoundSizeTuner: increased to {} files/compound", cur + 1);
288 }
289 }
290 }
291
292 fn on_size_error(&self) {
294 let cur = self.current_max.load(Ordering::Relaxed);
295 self.current_max.store((cur / 2).max(1), Ordering::Relaxed);
296 self.consecutive_success.store(0, Ordering::Relaxed);
297 warn!("CompoundSizeTuner: halved to {} files/compound", (cur / 2).max(1));
298 }
299}
300
301pub struct NfsCompoundClient {
306 stream: NfsTransport,
307 session_id: [u8; 16],
308 sequence_id: AtomicU32,
309 client_id: u64,
310 uid: u32,
311 gid: u32,
312 machine: String,
313 xid_counter: AtomicU32,
314 server_info: NfsBypassInfo,
315 dir_handle_cache: DashMap<PathBuf, Vec<u8>>,
317 compound_tuner: CompoundSizeTuner,
319 #[cfg(feature = "krb5")]
321 gss_ctx: Option<super::gss::GssContext>,
322}
323
324#[cfg(target_os = "linux")]
327fn set_quickack(stream: &NfsTransport) {
328 use std::os::unix::io::AsRawFd;
329 let val: libc::c_int = 1;
330 let ret = unsafe {
332 libc::setsockopt(
333 stream.tcp_ref().as_raw_fd(),
334 libc::IPPROTO_TCP,
335 libc::TCP_QUICKACK,
336 &val as *const _ as *const libc::c_void,
337 std::mem::size_of_val(&val) as libc::socklen_t,
338 )
339 };
340 if ret != 0 {
341 tracing::debug!("TCP_QUICKACK setsockopt failed: {}", std::io::Error::last_os_error());
342 }
343}
344
345#[cfg(not(target_os = "linux"))]
346fn set_quickack(_stream: &NfsTransport) {}
347
348impl NfsCompoundClient {
349 pub fn connect(info: &NfsBypassInfo) -> Result<Self, NfsError> {
353 let tcp_stream = Self::connect_privileged(&info.server_addr)?;
356 let stream = NfsTransport::new(tcp_stream);
357 stream.configure_tcp()?;
358 set_quickack(&stream);
360
361 let uid = unsafe { libc::getuid() };
365 let gid = unsafe { libc::getgid() };
367 let machine = {
368 let mut buf = [0u8; constants::NFS_HOSTNAME_BUFFER_SIZE];
369 if unsafe { libc::gethostname(buf.as_mut_ptr() as *mut _, buf.len()) } == 0 {
372 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
373 String::from_utf8_lossy(&buf[..len]).to_string()
374 } else {
375 "foxing".to_string()
376 }
377 };
378
379 let mut client = Self {
380 stream,
381 session_id: [0u8; 16],
382 sequence_id: AtomicU32::new(1),
383 client_id: 0,
384 uid,
385 gid,
386 machine,
387 xid_counter: AtomicU32::new(1),
388 server_info: info.clone(),
389 dir_handle_cache: DashMap::new(),
390 compound_tuner: CompoundSizeTuner::new(crate::constants::NFS_COMPOUND_TUNER_INITIAL_MAX_OPS),
391 #[cfg(feature = "krb5")]
392 gss_ctx: None,
393 };
394
395 #[cfg(feature = "tls")]
397 {
398 use super::NfsTransportSecurity;
399 match info.transport_security {
400 NfsTransportSecurity::Tls => {
401 client.try_tls_upgrade()?;
402 info!("NFS bypass: TLS transport active (xprtsec=tls)");
403 }
404 NfsTransportSecurity::Opportunistic => {
405 match client.try_tls_upgrade() {
406 Ok(()) => info!("NFS bypass: TLS transport active (opportunistic)"),
407 Err(NfsError::TlsNotSupported) => {
408 warn!(
409 "NFS bypass: server {} does not support xprtsec=tls, \
410 continuing with cleartext transport",
411 info.server_addr
412 );
413 crate::metrics::NFS_BYPASS_TLS_FALLBACK.inc();
414 }
415 Err(e) => {
416 warn!("NFS bypass: TLS opportunistic upgrade failed ({}), \
417 continuing cleartext", e);
418 crate::metrics::NFS_BYPASS_TLS_FALLBACK.inc();
419 }
420 }
421 }
422 NfsTransportSecurity::None => {}
423 }
424 }
425
426 #[cfg(all(feature = "tls", feature = "krb5"))]
428 if info.security == super::NfsSecurity::Krb5p
429 && info.transport_security != super::NfsTransportSecurity::None
430 {
431 warn!(
432 "NFS bypass: double encryption detected -- sec=krb5p (per-RPC AES-256) AND \
433 xprtsec=tls (TLS 1.3 AES-GCM) are both active. \
434 Consider sec=krb5,xprtsec=tls for ~5% overhead instead of ~30%. \
435 Both layers provide full confidentiality; combining adds no security gain."
436 );
437 crate::metrics::NFS_BYPASS_DOUBLE_ENCRYPTION.inc();
438 }
439
440 client.establish_session()?;
442
443 #[cfg(feature = "krb5")]
446 if info.security.requires_kerberos() {
447 #[cfg(feature = "tls")]
449 let channel_binding = client.stream.export_channel_binding();
450 #[cfg(not(feature = "tls"))]
451 let channel_binding: Option<Vec<u8>> = None;
452
453 let hostname = info.server_hostname.clone();
454 match super::gss::GssContext::establish(
455 &hostname,
456 info.security,
457 channel_binding.as_deref(),
458 ) {
459 Ok(mut gss) => {
460 match client.do_rpcsec_gss_init(&mut gss) {
461 Ok(()) => {
462 info!(
463 "NFS bypass: RPCSEC_GSS context ready (sec={}, server_handle={} bytes)",
464 info.security, gss.server_handle.len()
465 );
466 client.gss_ctx = Some(gss);
467 }
468 Err(e) => {
469 warn!(
470 "NFS bypass: RPCSEC_GSS INIT failed for {} (sec={}): {} -- \
471 falling back to AUTH_SYS",
472 info.server_addr, info.security, e
473 );
474 }
475 }
476 }
477 Err(e) => {
478 warn!(
479 "NFS bypass: GSS context failed for {} (sec={}): {} -- \
480 falling back to AUTH_SYS (may be rejected by server)",
481 info.server_addr, info.security, e
482 );
483 }
484 }
485 }
486
487 info!("NFS bypass: session established with {} (client_id={:#x})",
488 info.server_addr, client.client_id);
489
490 Ok(client)
491 }
492
493 fn establish_session(&mut self) -> Result<(), NfsError> {
495 let client_id = self.do_exchange_id()?;
497 self.client_id = client_id;
498 debug!("NFS bypass: EXCHANGE_ID ok, client_id={:#x}", client_id);
499
500 let session_id = self.do_create_session(client_id)?;
502 self.session_id = session_id;
503 self.sequence_id.store(1, Ordering::Relaxed);
504 debug!("NFS bypass: CREATE_SESSION ok, session={:02x?}", &session_id[..4]);
505
506 self.do_reclaim_complete()?;
509 debug!("NFS bypass: RECLAIM_COMPLETE ok");
510
511 Ok(())
512 }
513
514 fn do_exchange_id(&mut self) -> Result<u64, NfsError> {
516 let xid = self.next_xid();
517
518 let mut body = super::xdr::XdrEncoder::new(constants::NFS_XDR_ENCODER_SMALL_CAPACITY);
520
521 body.encode_u32(xid);
523 body.encode_u32(0); body.encode_u32(rpc::RPC_VERSION);
525 body.encode_u32(rpc::NFS_PROGRAM);
526 body.encode_u32(rpc::NFS_V4);
527 body.encode_u32(rpc::NFSPROC4_COMPOUND);
528
529 encode_auth_sys(&mut body, self.uid, self.gid, &self.machine);
531 body.encode_u32(rpc::AUTH_NONE);
533 body.encode_u32(0);
534
535 body.encode_string("exid");
537 body.encode_u32(2); body.encode_u32(1); body.encode_u32(rpc::OP_EXCHANGE_ID);
542 let verifier = std::time::SystemTime::now()
544 .duration_since(std::time::UNIX_EPOCH)
545 .unwrap_or_default()
546 .as_nanos() as u64;
547 body.encode_u64(verifier); let owner_id = format!("foxing.{}.{}", self.machine, std::process::id());
549 body.encode_opaque(owner_id.as_bytes()); body.encode_u32(0x00000001); body.encode_u32(0); body.encode_u32(0); let body_bytes = body.into_bytes();
558 let reply_data = self.rpc_call(xid, &body_bytes)?;
559 let reply = rpc::parse_compound_reply(&reply_data)?;
560
561 if reply.status != rpc::NFS4_OK {
562 return Err(NfsError::SessionFailed(format!(
563 "EXCHANGE_ID failed: {} ({})", rpc::nfs4_error_name(reply.status), reply.status
564 )));
565 }
566
567 let client_id = self.extract_exchange_id_client_id(&reply_data)?;
568 Ok(client_id)
569 }
570
571 fn extract_exchange_id_client_id(&self, data: &[u8]) -> Result<u64, NfsError> {
573 let needle = [
579 0, 0, 0, rpc::OP_EXCHANGE_ID as u8, 0, 0, 0, 0, ];
582 if let Some(pos) = data.windows(8).position(|w| w == needle) {
583 let cid_start = pos + 8;
584 if cid_start + 8 <= data.len() {
585 let client_id = u64::from_be_bytes([
586 data[cid_start], data[cid_start+1], data[cid_start+2], data[cid_start+3],
587 data[cid_start+4], data[cid_start+5], data[cid_start+6], data[cid_start+7],
588 ]);
589 return Ok(client_id);
590 }
591 }
592 Err(NfsError::SessionFailed("cannot parse client_id from EXCHANGE_ID reply".into()))
593 }
594
595 fn do_create_session(&mut self, client_id: u64) -> Result<[u8; 16], NfsError> {
597 let xid = self.next_xid();
598
599 let mut body = super::xdr::XdrEncoder::new(constants::NFS_XDR_ENCODER_SMALL_CAPACITY);
600
601 body.encode_u32(xid);
603 body.encode_u32(0); body.encode_u32(rpc::RPC_VERSION);
605 body.encode_u32(rpc::NFS_PROGRAM);
606 body.encode_u32(rpc::NFS_V4);
607 body.encode_u32(rpc::NFSPROC4_COMPOUND);
608
609 encode_auth_sys(&mut body, self.uid, self.gid, &self.machine);
611 body.encode_u32(rpc::AUTH_NONE);
612 body.encode_u32(0);
613
614 body.encode_string("csess");
616 body.encode_u32(2); body.encode_u32(1); body.encode_u32(rpc::OP_CREATE_SESSION);
621 body.encode_u64(client_id); body.encode_u32(1); body.encode_u32(0); body.encode_u32(0); body.encode_u32(crate::constants::NFS_MAX_REQUEST_SIZE as u32); body.encode_u32(crate::constants::NFS_MAX_RESPONSE_SIZE as u32); body.encode_u32(4096); body.encode_u32(crate::constants::NFS_CA_MAX_OPERATIONS); body.encode_u32(1); body.encode_u32(0); body.encode_u32(0); body.encode_u32(4096); body.encode_u32(4096); body.encode_u32(0); body.encode_u32(2); body.encode_u32(0); body.encode_u32(0); body.encode_u32(0x40000000); body.encode_u32(1); body.encode_u32(rpc::AUTH_SYS); body.encode_u32(0); body.encode_string(&self.machine); body.encode_u32(self.uid); body.encode_u32(self.gid); body.encode_u32(1); body.encode_u32(self.gid); let body_bytes = body.into_bytes();
657 let reply_data = self.rpc_call(xid, &body_bytes)?;
658 let reply = rpc::parse_compound_reply(&reply_data)?;
659
660 if reply.status != rpc::NFS4_OK {
661 return Err(NfsError::SessionFailed(format!(
662 "CREATE_SESSION failed: {} ({})", rpc::nfs4_error_name(reply.status), reply.status
663 )));
664 }
665
666 let session_id = self.extract_session_id(&reply_data)?;
668
669 let needle = [
671 0, 0, 0, rpc::OP_CREATE_SESSION as u8,
672 0, 0, 0, 0,
673 ];
674 if let Some(pos) = reply_data.windows(8).position(|w| w == needle) {
675 let attrs_start = pos + 8 + 16 + 4 + 4; if attrs_start + 12 <= reply_data.len() {
679 let max_req = u32::from_be_bytes([
680 reply_data[attrs_start + 4], reply_data[attrs_start + 5],
681 reply_data[attrs_start + 6], reply_data[attrs_start + 7],
682 ]);
683 let max_ops = if attrs_start + 20 <= reply_data.len() {
684 u32::from_be_bytes([
685 reply_data[attrs_start + 16], reply_data[attrs_start + 17],
686 reply_data[attrs_start + 18], reply_data[attrs_start + 19],
687 ])
688 } else { 0 };
689 debug!(
690 "NFS bypass: server negotiated ca_maxrequestsize={} ca_maxoperations={}",
691 max_req, max_ops
692 );
693 }
694 }
695
696 Ok(session_id)
697 }
698
699 fn extract_session_id(&self, data: &[u8]) -> Result<[u8; 16], NfsError> {
701 let needle = [
703 0, 0, 0, rpc::OP_CREATE_SESSION as u8,
704 0, 0, 0, 0,
705 ];
706 if let Some(pos) = data.windows(8).position(|w| w == needle) {
707 let sid_start = pos + 8;
708 if sid_start + 16 <= data.len() {
709 let mut session_id = [0u8; 16];
710 session_id.copy_from_slice(&data[sid_start..sid_start + 16]);
711 return Ok(session_id);
712 }
713 }
714 Err(NfsError::SessionFailed("cannot parse session_id from CREATE_SESSION reply".into()))
715 }
716
717 fn do_reclaim_complete(&mut self) -> Result<(), NfsError> {
719 let seq_id = self.next_sequence_id();
720 let ops = vec![
721 Nfs4Op::Sequence {
722 session_id: self.session_id,
723 sequence_id: seq_id,
724 slot_id: 0,
725 highest_slot_id: 0,
726 cache_this: false,
727 },
728 Nfs4Op::ReclaimComplete,
729 ];
730 let xid = self.next_xid();
731 let msg = self.build_compound_msg(xid, "rclm", &ops)?;
732 let reply_data = self.rpc_call(xid, &msg)?;
733 let reply = rpc::parse_compound_reply(&reply_data)?;
734 if reply.status != rpc::NFS4_OK {
735 return Err(NfsError::SessionFailed(format!(
736 "RECLAIM_COMPLETE failed: {} ({})", rpc::nfs4_error_name(reply.status), reply.status
737 )));
738 }
739 Ok(())
740 }
741
742 pub fn recover_session(&mut self) -> Result<(), NfsError> {
744 warn!("NFS bypass: recovering session...");
745 let new_tcp = TcpStream::connect_timeout(
747 &self.server_info.server_addr,
748 std::time::Duration::from_secs(constants::NFS_TCP_CONNECT_TIMEOUT_SECS),
749 )?;
750 self.stream = NfsTransport::new(new_tcp);
751 self.stream.configure_tcp()?;
752 set_quickack(&self.stream);
753
754 #[cfg(feature = "tls")]
756 {
757 use super::NfsTransportSecurity;
758 match self.server_info.transport_security {
759 NfsTransportSecurity::Tls => {
760 self.try_tls_upgrade()?;
761 debug!("NFS recovery: TLS re-established");
762 }
763 NfsTransportSecurity::Opportunistic => {
764 match self.try_tls_upgrade() {
765 Ok(()) => debug!("NFS recovery: TLS re-established (opportunistic)"),
766 Err(e) => {
767 warn!("NFS recovery: TLS re-upgrade failed ({}), continuing cleartext", e);
768 crate::metrics::NFS_BYPASS_TLS_FALLBACK.inc();
769 }
770 }
771 }
772 NfsTransportSecurity::None => {}
773 }
774 }
775
776 self.establish_session()?;
778
779 self.dir_handle_cache.clear();
781
782 #[cfg(feature = "krb5")]
784 if self.server_info.security.requires_kerberos() {
785 #[cfg(feature = "tls")]
786 let channel_binding = self.stream.export_channel_binding();
787 #[cfg(not(feature = "tls"))]
788 let channel_binding: Option<Vec<u8>> = None;
789
790 let hostname = self.server_info.server_hostname.clone();
791 match super::gss::GssContext::establish(
792 &hostname,
793 self.server_info.security,
794 channel_binding.as_deref(),
795 ) {
796 Ok(mut gss) => {
797 match self.do_rpcsec_gss_init(&mut gss) {
798 Ok(()) => {
799 self.gss_ctx = Some(gss);
800 debug!("NFS bypass: GSS context re-established and INIT complete after recovery");
801 }
802 Err(e) => {
803 warn!("NFS bypass: RPCSEC_GSS INIT failed after recovery: {} -- AUTH_SYS fallback", e);
804 self.gss_ctx = None;
805 }
806 }
807 }
808 Err(e) => {
809 warn!("NFS bypass: GSS re-establishment failed: {} -- AUTH_SYS fallback", e);
810 self.gss_ctx = None;
811 }
812 }
813 }
814
815 info!("NFS bypass: session recovered (client_id={:#x})", self.client_id);
816 Ok(())
817 }
818
819 pub fn ensure_gss_valid(&mut self) {
822 #[cfg(feature = "krb5")]
823 {
824 let needs_refresh = self.gss_ctx.as_mut().map(|g| !g.is_valid()).unwrap_or(false);
825 if needs_refresh {
826 self.refresh_gss_context();
827 }
828 }
829 }
830
831 fn unwrap_reply(&mut self, data: Vec<u8>) -> Result<Vec<u8>, NfsError> {
834 #[cfg(feature = "krb5")]
835 if let Some(ref mut gss) = self.gss_ctx {
836 return rpc::unwrap_gss_reply(&data, gss);
837 }
838 Ok(data)
839 }
840
841 fn build_compound_msg(&mut self, xid: u32, tag: &str, ops: &[Nfs4Op]) -> Result<Vec<u8>, NfsError> {
842 #[cfg(feature = "krb5")]
843 {
844 let needs_refresh = self.gss_ctx.as_mut().map(|g| !g.is_valid()).unwrap_or(false);
845 if needs_refresh {
846 self.refresh_gss_context();
847 }
848 if let Some(ref mut gss) = self.gss_ctx {
849 return rpc::build_compound_gss(xid, tag, ops, gss);
850 }
851 }
852 Ok(rpc::build_compound(xid, tag, self.uid, self.gid, &self.machine, ops))
853 }
854
855 #[cfg(feature = "krb5")]
858 fn do_rpcsec_gss_init(&mut self, gss: &mut super::gss::GssContext) -> Result<(), NfsError> {
859 const GSS_S_COMPLETE: u32 = 0;
860 const GSS_S_CONTINUE_NEEDED: u32 = 1;
861
862 let mut handle = Vec::<u8>::new();
863 let mut token = gss.init_token.clone();
864
865 loop {
866 let xid = self.next_xid();
867 let msg = rpc::build_rpcsec_gss_init(xid, &handle, &token);
868 let reply = self.rpc_call(xid, &msg)?;
869 let (server_handle, gss_major, gss_minor, _seq_window, cont_token) =
870 rpc::parse_rpcsec_gss_init_reply(&reply)?;
871
872 handle = server_handle;
873
874 match gss_major {
875 GSS_S_COMPLETE => {
876 if !cont_token.is_empty() {
879 let _ = gss.step(&cont_token);
880 }
881 gss.server_handle = handle;
882 debug!(
883 "RPCSEC_GSS INIT complete, handle={} bytes",
884 gss.server_handle.len()
885 );
886 return Ok(());
887 }
888 GSS_S_CONTINUE_NEEDED => {
889 match gss.step(&cont_token)? {
890 Some(new_token) => {
891 token = new_token;
892 }
893 None => {
894 return Err(NfsError::RpcError(
895 "GSS_S_CONTINUE_NEEDED but no output token from gss.step()"
896 .into(),
897 ));
898 }
899 }
900 }
901 _ => {
902 return Err(NfsError::RpcError(format!(
903 "RPCSEC_GSS INIT failed: major={} minor={}",
904 gss_major, gss_minor
905 )));
906 }
907 }
908 }
909 }
910
911 #[cfg(feature = "krb5")]
914 fn refresh_gss_context(&mut self) {
915 #[cfg(feature = "tls")]
916 let channel_binding = self.stream.export_channel_binding();
917 #[cfg(not(feature = "tls"))]
918 let channel_binding: Option<Vec<u8>> = None;
919
920 let hostname = self.server_info.server_hostname.clone();
921 match super::gss::GssContext::establish(
922 &hostname,
923 self.server_info.security,
924 channel_binding.as_deref(),
925 ) {
926 Ok(mut new_ctx) => {
927 match self.do_rpcsec_gss_init(&mut new_ctx) {
928 Ok(()) => {
929 debug!("NFS bypass: GSS context refreshed (sec={})", self.server_info.security);
930 self.gss_ctx = Some(new_ctx);
931 }
932 Err(e) => {
933 warn!("NFS bypass: GSS context refresh INIT failed: {e} -- falling back to AUTH_SYS");
934 self.gss_ctx = None;
935 }
936 }
937 }
938 Err(e) => {
939 warn!("NFS bypass: GSS context refresh failed: {e} -- falling back to AUTH_SYS");
940 self.gss_ctx = None;
941 }
942 }
943 }
944
945 fn connect_privileged(server: &std::net::SocketAddr) -> Result<TcpStream, NfsError> {
948 use std::os::unix::io::FromRawFd;
949
950 let domain = if server.is_ipv4() { libc::AF_INET } else { libc::AF_INET6 };
951 let fd = unsafe { libc::socket(domain, libc::SOCK_STREAM, 0) };
953 if fd < 0 {
954 return Err(NfsError::ConnectionFailed(std::io::Error::last_os_error()));
955 }
956
957 let mut bound = false;
959 for port in (constants::NFS_PRIVILEGED_PORT_START..constants::NFS_PRIVILEGED_PORT_END).rev() {
960 let ret = if server.is_ipv4() {
961 let addr = libc::sockaddr_in {
962 sin_family: libc::AF_INET as u16,
963 sin_port: port.to_be(),
964 sin_addr: libc::in_addr { s_addr: 0 },
965 sin_zero: [0; 8],
966 };
967 unsafe { libc::bind(fd, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_in>() as u32) }
969 } else {
970 let addr = libc::sockaddr_in6 {
971 sin6_family: libc::AF_INET6 as u16,
972 sin6_port: port.to_be(),
973 sin6_flowinfo: 0,
974 sin6_addr: libc::in6_addr { s6_addr: [0; 16] },
975 sin6_scope_id: 0,
976 };
977 unsafe { libc::bind(fd, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_in6>() as u32) }
979 };
980 if ret == 0 {
981 bound = true;
982 debug!("NFS bypass: bound to privileged port {}", port);
983 break;
984 }
985 }
986
987 if !bound {
988 unsafe { libc::close(fd); }
990 debug!("NFS bypass: no privileged port available, using ephemeral");
992 return Ok(TcpStream::connect_timeout(server, std::time::Duration::from_secs(constants::NFS_TCP_CONNECT_TIMEOUT_SECS))?);
993 }
994
995 let connect_result = match server {
997 std::net::SocketAddr::V4(v4) => {
998 let addr = libc::sockaddr_in {
999 sin_family: libc::AF_INET as u16,
1000 sin_port: v4.port().to_be(),
1001 sin_addr: libc::in_addr { s_addr: u32::from_ne_bytes(v4.ip().octets()) },
1002 sin_zero: [0; 8],
1003 };
1004 unsafe { libc::connect(fd, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_in>() as u32) }
1006 }
1007 std::net::SocketAddr::V6(v6) => {
1008 let addr = libc::sockaddr_in6 {
1009 sin6_family: libc::AF_INET6 as u16,
1010 sin6_port: v6.port().to_be(),
1011 sin6_flowinfo: v6.flowinfo(),
1012 sin6_addr: libc::in6_addr { s6_addr: v6.ip().octets() },
1013 sin6_scope_id: v6.scope_id(),
1014 };
1015 unsafe { libc::connect(fd, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_in6>() as u32) }
1017 }
1018 };
1019
1020 if connect_result != 0 {
1021 let err = std::io::Error::last_os_error();
1022 unsafe { libc::close(fd); }
1024 return Err(NfsError::ConnectionFailed(err));
1025 }
1026
1027 Ok(unsafe { TcpStream::from_raw_fd(fd) })
1029 }
1030
1031 #[cfg(feature = "tls")]
1041 fn try_tls_upgrade(&mut self) -> Result<(), NfsError> {
1042 use rustls::pki_types::ServerName;
1043 use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
1044 use std::sync::Arc;
1045
1046 let probe_xid = self.next_xid();
1048 let probe = super::rpc::build_auth_tls_probe(probe_xid);
1049 let reply = self.rpc_call(probe_xid, &probe)
1050 .map_err(|e| NfsError::TlsHandshakeFailed(format!("AUTH_TLS probe: {e}")))?;
1051
1052 if !super::rpc::parse_auth_tls_reply(&reply) {
1054 return Err(NfsError::TlsNotSupported);
1055 }
1056
1057 info!(
1058 "NFS TLS: server {} supports RPC-over-TLS, upgrading connection (RFC 9289)",
1059 self.server_info.server_addr
1060 );
1061
1062 let roots = {
1064 let mut store = RootCertStore::empty();
1065 for cert in rustls_native_certs::load_native_certs().certs {
1066 let _ = store.add(cert);
1067 }
1068 store
1069 };
1070 let mut config = ClientConfig::builder()
1071 .with_root_certificates(roots)
1072 .with_no_client_auth();
1073 config.alpn_protocols = vec![constants::NFS_TLS_ALPN.to_vec()];
1074 #[cfg(feature = "ktls")]
1075 {
1076 config.enable_secret_extraction = true;
1077 }
1078 let config = Arc::new(config);
1079
1080 let hostname = &self.server_info.server_hostname;
1082 let server_name = ServerName::try_from(hostname.as_str())
1083 .map_err(|e| {
1084 NfsError::TlsHandshakeFailed(format!(
1085 "invalid server hostname '{}' for TLS SNI: {e}",
1086 hostname
1087 ))
1088 })?
1089 .to_owned();
1090
1091 let tcp_clone = self
1095 .stream
1096 .tcp_ref()
1097 .try_clone()
1098 .map_err(|e| NfsError::TlsHandshakeFailed(format!("TCP clone: {e}")))?;
1099
1100 let conn = ClientConnection::new(config, server_name)
1103 .map_err(|e| NfsError::TlsHandshakeFailed(format!("rustls init: {e}")))?;
1104 let tls_stream = StreamOwned::new(conn, tcp_clone);
1105
1106 #[cfg(feature = "ktls")]
1117 {
1118 let (transport, promoted) = Self::try_ktls_or_tls(tls_stream);
1119 self.stream = transport;
1120 if promoted {
1121 info!(
1122 "NFS TLS: kTLS kernel offload active -- AES-GCM in-kernel, \
1123 server={}",
1124 self.server_info.server_addr
1125 );
1126 crate::metrics::NFS_BYPASS_KTLS_PROMOTED.inc();
1127 } else {
1128 info!(
1129 "NFS TLS: TLS 1.3 handshake ready (will complete on first \
1130 EXCHANGE_ID write), server={}, alpn=sunrpc",
1131 self.server_info.server_addr
1132 );
1133 }
1134 return Ok(());
1135 }
1136
1137 #[cfg(not(feature = "ktls"))]
1139 {
1140 self.stream = NfsTransport::Tls(Box::new(tls_stream));
1141 info!(
1142 "NFS TLS: TLS 1.3 handshake ready (will complete on first EXCHANGE_ID write), \
1143 server={}, alpn=sunrpc",
1144 self.server_info.server_addr
1145 );
1146 }
1147 Ok(())
1148 }
1149
1150 #[cfg(feature = "ktls")]
1155 fn try_ktls_or_tls(
1156 mut tls_stream: rustls::StreamOwned<rustls::ClientConnection, TcpStream>,
1157 ) -> (NfsTransport, bool) {
1158 use rustls::StreamOwned;
1159 use std::os::unix::io::AsRawFd;
1160
1161 if let Err(e) = tls_stream.conn.complete_io(&mut tls_stream.sock) {
1163 debug!("NFS TLS: kTLS skipped -- handshake completion failed ({e})");
1164 return (NfsTransport::Tls(Box::new(tls_stream)), false);
1165 }
1166
1167 let fd = tls_stream.sock.as_raw_fd();
1168
1169 const SOL_TCP: libc::c_int = 6;
1172 const TCP_ULP_OPT: libc::c_int = 31;
1173 let ret = unsafe {
1174 libc::setsockopt(fd, SOL_TCP, TCP_ULP_OPT, b"tls\0".as_ptr().cast(), 4)
1175 };
1176 if ret < 0 {
1177 let err = std::io::Error::last_os_error();
1178 debug!("NFS TLS: kTLS ULP unavailable ({err}), using userspace TLS");
1179 return (NfsTransport::Tls(Box::new(tls_stream)), false);
1180 }
1181
1182 let suite = match tls_stream.conn.negotiated_cipher_suite() {
1184 Some(s) => s,
1185 None => {
1186 debug!("NFS TLS: kTLS skipped -- no negotiated cipher suite");
1187 return (NfsTransport::Tls(Box::new(tls_stream)), false);
1188 }
1189 };
1190
1191 let StreamOwned { conn, sock: tcp } = tls_stream;
1195
1196 let secrets = match conn.dangerous_extract_secrets() {
1197 Ok(s) => s,
1198 Err(e) => {
1199 debug!("NFS TLS: kTLS secret extraction failed ({e}), degraded to plain TCP");
1200 return (NfsTransport::Tcp(tcp), false);
1201 }
1202 };
1203
1204 let tx_info = match ktls::CryptoInfo::from_rustls(suite, secrets.tx) {
1206 Ok(i) => i,
1207 Err(e) => {
1208 debug!("NFS TLS: kTLS TX crypto unsupported ({e:?}), degraded to plain TCP");
1209 return (NfsTransport::Tcp(tcp), false);
1210 }
1211 };
1212 let rx_info = match ktls::CryptoInfo::from_rustls(suite, secrets.rx) {
1213 Ok(i) => i,
1214 Err(e) => {
1215 debug!("NFS TLS: kTLS RX crypto unsupported ({e:?}), degraded to plain TCP");
1216 return (NfsTransport::Tcp(tcp), false);
1217 }
1218 };
1219
1220 const SOL_TLS: libc::c_int = 282;
1222 const TLS_TX: libc::c_int = 1;
1223 const TLS_RX: libc::c_int = 2;
1224
1225 let tx_ret = unsafe {
1226 libc::setsockopt(
1227 fd, SOL_TLS, TLS_TX,
1228 tx_info.as_ptr(), tx_info.size() as libc::socklen_t,
1229 )
1230 };
1231 if tx_ret < 0 {
1232 let err = std::io::Error::last_os_error();
1233 debug!("NFS TLS: kTLS TX setsockopt failed ({err}), degraded to plain TCP");
1234 return (NfsTransport::Tcp(tcp), false);
1235 }
1236
1237 let rx_ret = unsafe {
1238 libc::setsockopt(
1239 fd, SOL_TLS, TLS_RX,
1240 rx_info.as_ptr(), rx_info.size() as libc::socklen_t,
1241 )
1242 };
1243 if rx_ret < 0 {
1244 let err = std::io::Error::last_os_error();
1245 debug!("NFS TLS: kTLS RX setsockopt failed ({err}), TX active but RX not -- degraded");
1246 return (NfsTransport::Tcp(tcp), false);
1247 }
1248
1249 (NfsTransport::Ktls(tcp), true)
1250 }
1251
1252 fn next_xid(&self) -> u32 {
1253 self.xid_counter.fetch_add(1, Ordering::Relaxed)
1254 }
1255
1256 fn next_sequence_id(&self) -> u32 {
1257 self.sequence_id.fetch_add(1, Ordering::Relaxed)
1258 }
1259
1260 fn rpc_call(&mut self, _xid: u32, body: &[u8]) -> Result<Vec<u8>, NfsError> {
1270 #[cfg(feature = "rdma")]
1274 if let NfsTransport::Rdma(ref mut rdma) = self.stream {
1275 return rdma.rpc_call(_xid, body);
1276 }
1277
1278 let frame = rpc::frame_rpc_message(body);
1279 self.stream.write_all(&frame)?;
1280 self.stream.flush()?;
1281 self.read_reply()
1282 }
1283
1284 pub fn write_file(
1289 &mut self,
1290 parent_handle: &[u8],
1291 filename: &str,
1292 data: &[u8],
1293 mode: u32,
1294 uid: u32,
1295 gid: u32,
1296 mtime: (i64, i64),
1297 ) -> Result<(), NfsError> {
1298 for attempt in 0..constants::NFS_GRACE_RETRY_COUNT {
1299 match self.write_file_inner(parent_handle, filename, data, mode, uid, gid, mtime) {
1300 Ok(()) => return Ok(()),
1301 Err(NfsError::Nfs4Error { code: 10013, .. }) => {
1302 if attempt < constants::NFS_GRACE_RETRY_COUNT - 1 {
1304 debug!("NFS bypass: server in grace period, retry {} in {}ms", attempt + 1, constants::NFS_RECONNECT_SLEEP_MS);
1305 std::thread::sleep(std::time::Duration::from_millis(constants::NFS_RECONNECT_SLEEP_MS));
1306 continue;
1307 }
1308 return Err(NfsError::Nfs4Error {
1309 code: 10013,
1310 message: "server grace period persists after retries".into(),
1311 });
1312 }
1313 Err(e) => return Err(e),
1314 }
1315 }
1316 Err(NfsError::Nfs4Error {
1317 code: 10013,
1318 message: "server grace period persists after retries".into(),
1319 })
1320 }
1321
1322 fn write_file_inner(
1323 &mut self,
1324 parent_handle: &[u8],
1325 filename: &str,
1326 data: &[u8],
1327 mode: u32,
1328 _uid: u32,
1329 _gid: u32,
1330 _mtime: (i64, i64),
1331 ) -> Result<(), NfsError> {
1332 #[cfg(feature = "krb5")]
1333 if let Some(ref gss) = self.gss_ctx {
1334 if gss.rpc_gss_service() == super::gss::RpcGssService::Privacy
1335 && data.len() > constants::KRB5P_MAX_WRITE_DATA
1336 {
1337 return Err(NfsError::Nfs4Error {
1338 code: rpc::NFS4ERR_INVAL,
1339 message: format!(
1340 "krb5p bypass: {}B exceeds {}B threshold (AES block boundary at priv_bytes=3952)",
1341 data.len(), constants::KRB5P_MAX_WRITE_DATA
1342 ),
1343 });
1344 }
1345 }
1346
1347 let xid = self.next_xid();
1348 let seq_id = self.next_sequence_id();
1349
1350 let ops = vec![
1351 Nfs4Op::Sequence {
1352 session_id: self.session_id,
1353 sequence_id: seq_id,
1354 slot_id: 0,
1355 highest_slot_id: 0,
1356 cache_this: false,
1357 },
1358 Nfs4Op::PutFh { handle: parent_handle.to_vec() },
1359 Nfs4Op::Open {
1360 seqid: 0,
1361 share_access: rpc::OPEN4_SHARE_ACCESS_WRITE,
1362 share_deny: rpc::OPEN4_SHARE_DENY_NONE,
1363 clientid: self.client_id,
1364 owner: format!("foxing-{}", std::process::id()).into_bytes(),
1365 filename: filename.to_string(),
1366 mode,
1367 create_owner: None,
1368 create_group: None,
1369 create_mtime: None,
1370 },
1371 Nfs4Op::Write {
1372 stateid: StateId::current(),
1373 offset: 0,
1374 stable: WriteStable::FileSync,
1375 data: data.to_vec(),
1376 },
1377 Nfs4Op::Close {
1378 seqid: 1,
1379 stateid: StateId::current(),
1380 },
1381 ];
1382
1383 let msg = self.build_compound_msg(xid, "fxcp", &ops)?;
1384
1385 debug!("NFS write compound: {} bytes, {} ops, handle={} bytes, data={} bytes",
1386 msg.len(), ops.len(), parent_handle.len(), data.len());
1387 if msg.len() > 140 {
1388 debug!("NFS compound hex (first 200 bytes): {:02x?}", &msg[..msg.len().min(200)]);
1389 }
1390
1391 let reply_data = self.rpc_call(xid, &msg)?;
1392 let reply_data = self.unwrap_reply(reply_data)?;
1393 let reply = rpc::parse_compound_reply(&reply_data)?;
1394
1395 debug!("NFS compound reply: overall_status={} ({}) ops={}",
1397 reply.status, rpc::nfs4_error_name(reply.status), reply.op_results.len());
1398 for (i, r) in reply.op_results.iter().enumerate() {
1399 debug!(" op[{}]: op={} status={} ({})", i, r.op, r.status, rpc::nfs4_error_name(r.status));
1400 }
1401
1402 if reply.status != rpc::NFS4_OK {
1403 let code = reply.status;
1404 if code == rpc::NFS4ERR_BADSESSION || code == rpc::NFS4ERR_BADSEQ {
1406 return Err(NfsError::Nfs4Error {
1407 code,
1408 message: format!("session error: {} -- needs recovery", rpc::nfs4_error_name(code)),
1409 });
1410 }
1411 if code == rpc::NFS4ERR_STALE {
1412 return Err(NfsError::StaleHandle {
1413 path: filename.to_string(),
1414 });
1415 }
1416 return Err(NfsError::Nfs4Error {
1417 code,
1418 message: format!("compound failed: {}", rpc::nfs4_error_name(code)),
1419 });
1420 }
1421
1422 for result in &reply.op_results {
1424 if result.status != rpc::NFS4_OK {
1425 return Err(NfsError::Nfs4Error {
1426 code: result.status,
1427 message: format!("op {} failed: {}", result.op, rpc::nfs4_error_name(result.status)),
1428 });
1429 }
1430 }
1431
1432 debug!("NFS bypass: wrote {} ({} bytes) via compound RPC", filename, data.len());
1433 Ok(())
1434 }
1435
1436 pub fn write_files_compound(
1445 &mut self,
1446 parent_handle: &[u8],
1447 files: &[(&str, &[u8], u32, u32, u32, (i64, i64))], ) -> Result<Vec<Result<(), NfsError>>, NfsError> {
1449 if files.is_empty() { return Ok(vec![]); }
1450
1451 let max_files = self.compound_tuner.current();
1454 #[cfg(feature = "krb5")]
1458 let compound_byte_limit: usize = if self.gss_ctx.as_ref()
1459 .map(|g| g.rpc_gss_service() == super::gss::RpcGssService::Privacy)
1460 .unwrap_or(false)
1461 {
1462 constants::KRB5P_MAX_WRITE_DATA + constants::GSS_WRAP_OVERHEAD_BYTES
1463 } else {
1464 constants::NFS_COMPOUND_BYTE_LIMIT
1465 };
1466 #[cfg(not(feature = "krb5"))]
1467 let compound_byte_limit: usize = constants::NFS_COMPOUND_BYTE_LIMIT;
1468
1469 let effective_max = {
1470 let mut byte_acc: usize = constants::NFS_XDR_HEADER_OVERHEAD;
1471 let mut count = 0usize;
1472 for (_, data, ..) in files.iter() {
1473 byte_acc += data.len() + constants::NFS_XDR_PER_OP_OVERHEAD;
1474 if byte_acc > compound_byte_limit {
1475 break;
1476 }
1477 count += 1;
1478 }
1479 count.max(1).min(max_files)
1480 };
1481 if files.len() > effective_max {
1482 let mut all_results = Vec::with_capacity(files.len());
1483 for chunk in files.chunks(effective_max) {
1484 let results = self.write_files_compound(parent_handle, chunk)?;
1485 all_results.extend(results);
1486 }
1487 return Ok(all_results);
1488 }
1489
1490 let xid = self.next_xid();
1491 let seq_id = self.next_sequence_id();
1492
1493 let mut ops = vec![
1494 Nfs4Op::Sequence {
1495 session_id: self.session_id,
1496 sequence_id: seq_id,
1497 slot_id: 0,
1498 highest_slot_id: 0,
1499 cache_this: false,
1500 },
1501 Nfs4Op::PutFh { handle: parent_handle.to_vec() },
1502 ];
1503
1504 let owner = format!("foxing-{}", std::process::id()).into_bytes();
1505 for (filename, data, mode, _uid, _gid, _mtime) in files {
1506 ops.push(Nfs4Op::Open {
1507 seqid: 0,
1508 share_access: rpc::OPEN4_SHARE_ACCESS_WRITE,
1509 share_deny: rpc::OPEN4_SHARE_DENY_NONE,
1510 clientid: self.client_id,
1511 owner: owner.clone(),
1512 filename: filename.to_string(),
1513 mode: *mode,
1514 create_owner: None,
1515 create_group: None,
1516 create_mtime: None,
1517 });
1518 ops.push(Nfs4Op::Write {
1519 stateid: StateId::current(),
1520 offset: 0,
1521 stable: WriteStable::FileSync,
1522 data: data.to_vec(),
1523 });
1524 ops.push(Nfs4Op::Close {
1525 seqid: 1,
1526 stateid: StateId::current(),
1527 });
1528 }
1529
1530 let msg = self.build_compound_msg(xid, "mwrt", &ops)?;
1531 let reply_data = self.rpc_call(xid, &msg)?;
1532 let reply_data = self.unwrap_reply(reply_data)?;
1533 let reply = rpc::parse_compound_reply(&reply_data)?;
1534
1535 let mut results = Vec::with_capacity(files.len());
1539 for file_idx in 0..files.len() {
1540 let open_idx = 2 + file_idx * 3;
1541 let mut file_ok = true;
1542 for op_offset in 0..3 {
1543 let idx = open_idx + op_offset;
1544 match reply.op_results.get(idx) {
1545 Some(r) if r.status == rpc::NFS4_OK => {}
1546 Some(r) => {
1547 file_ok = false;
1548 if r.status == 10019 || r.status == 10028 {
1550 self.compound_tuner.on_size_error();
1551 }
1552 results.push(Err(NfsError::Nfs4Error {
1553 code: r.status,
1554 message: format!(
1555 "multi-write file {} op {} failed: {}",
1556 file_idx, r.op, rpc::nfs4_error_name(r.status)
1557 ),
1558 }));
1559 break;
1560 }
1561 None => {
1562 file_ok = false;
1563 results.push(Err(NfsError::Nfs4Error {
1564 code: reply.status,
1565 message: format!(
1566 "multi-write file {} truncated compound (prior error)",
1567 file_idx
1568 ),
1569 }));
1570 break;
1571 }
1572 }
1573 }
1574 if file_ok {
1575 debug!("NFS multi-write: file {} ({}) ok", file_idx, files[file_idx].0);
1576 results.push(Ok(()));
1577 }
1578 }
1579 if results.iter().all(|r| r.is_ok()) {
1580 self.compound_tuner.on_success();
1581 }
1582 Ok(results)
1583 }
1584
1585 fn read_reply(&mut self) -> Result<Vec<u8>, NfsError> {
1588 set_quickack(&self.stream);
1589 let mut result = Vec::with_capacity(constants::NFS_REPLY_INITIAL_CAPACITY);
1590
1591 loop {
1592 let mut rm_buf = [0u8; 4];
1593 self.stream.read_exact(&mut rm_buf)?;
1594 let rm = u32::from_be_bytes(rm_buf);
1595 let last_fragment = (rm & 0x80000000) != 0;
1596 let length = (rm & 0x7FFFFFFF) as usize;
1597
1598 if length > crate::constants::NFS_MAX_REQUEST_SIZE {
1599 return Err(NfsError::RpcError(format!("reply fragment too large: {} bytes", length)));
1600 }
1601
1602 if result.is_empty() {
1603 result.extend_from_slice(&rm_buf);
1605 }
1606
1607 let offset = result.len();
1608 result.resize(offset + length, 0);
1609 self.stream.read_exact(&mut result[offset..])?;
1610
1611 if last_fragment {
1612 break;
1613 }
1614 }
1615
1616 Ok(result)
1617 }
1618
1619 pub fn get_or_resolve_handle(&mut self, dir_path: &Path) -> Result<Vec<u8>, NfsError> {
1624 if let Some(cached) = self.dir_handle_cache.get(dir_path) {
1625 return Ok(cached.clone());
1626 }
1627
1628 let rel = dir_path.strip_prefix(&self.server_info.mount_point)
1630 .unwrap_or(std::path::Path::new(""));
1631
1632 let handle = self.resolve_via_lookup(rel)?;
1634
1635 self.dir_handle_cache.insert(dir_path.to_path_buf(), handle.clone());
1636 Ok(handle)
1637 }
1638
1639 fn resolve_via_lookup(&mut self, rel_path: &Path) -> Result<Vec<u8>, NfsError> {
1641 let xid = self.next_xid();
1642 let seq_id = self.next_sequence_id();
1643
1644 let mount_fh = super::mount::resolve_nfs_handle(&self.server_info.mount_point)
1648 .map_err(|e| NfsError::StaleHandle { path: format!("mount: {}", e) })?;
1649 debug!("NFS PUTFH: mount handle {} bytes: {:02x?}", mount_fh.len(), &mount_fh[..mount_fh.len().min(16)]);
1650
1651 let mut ops = vec![
1652 Nfs4Op::Sequence {
1653 session_id: self.session_id,
1654 sequence_id: seq_id,
1655 slot_id: 0,
1656 highest_slot_id: 0,
1657 cache_this: false,
1658 },
1659 Nfs4Op::PutFh { handle: mount_fh },
1660 ];
1661
1662 for component in rel_path.components() {
1664 if let std::path::Component::Normal(name) = component {
1665 ops.push(Nfs4Op::Lookup { name: name.to_string_lossy().into_owned() });
1666 }
1667 }
1668
1669 ops.push(Nfs4Op::GetFh);
1671
1672 let msg = self.build_compound_msg(xid, "lkup", &ops)?;
1673 let reply_data = self.rpc_call(xid, &msg)?;
1674 let reply_data = self.unwrap_reply(reply_data)?;
1675 let reply = rpc::parse_compound_reply(&reply_data)?;
1676
1677 debug!("NFS LOOKUP reply: overall_status={} ({}) ops={}",
1678 reply.status, rpc::nfs4_error_name(reply.status), reply.op_results.len());
1679 for (i, r) in reply.op_results.iter().enumerate() {
1680 debug!(" LOOKUP op[{}]: op={} status={} ({})", i, r.op, r.status, rpc::nfs4_error_name(r.status));
1681 }
1682
1683 if reply.status != rpc::NFS4_OK {
1684 return Err(NfsError::Nfs4Error {
1685 code: reply.status,
1686 message: format!("LOOKUP for {:?} failed: {}", rel_path, rpc::nfs4_error_name(reply.status)),
1687 });
1688 }
1689
1690 for result in &reply.op_results {
1692 if result.op == rpc::OP_GETFH
1693 && let Some(fh) = &result.filehandle {
1694 debug!("NFS GETFH: resolved {:?} -> {} bytes", rel_path, fh.len());
1695 return Ok(fh.clone());
1696 }
1697 }
1698
1699 Err(NfsError::SessionFailed(format!("GETFH not found in reply for {:?}", rel_path)))
1700 }
1701
1702 pub fn invalidate_handle(&self, dir_path: &Path) {
1704 self.dir_handle_cache.remove(dir_path);
1705 }
1706
1707 pub fn batch_stat(
1714 &mut self,
1715 dir_handle: &[u8],
1716 filenames: &[&str],
1717 ) -> Result<Vec<(String, u64, i64, i64)>, NfsError> {
1718 let xid = self.next_xid();
1719 let seq_id = self.next_sequence_id();
1720
1721 let attr_request = [1 << rpc::FATTR4_SIZE, 1 << (rpc::FATTR4_TIME_MODIFY - 32)];
1722
1723 let mut ops = vec![
1724 rpc::Nfs4Op::Sequence {
1725 session_id: self.session_id,
1726 sequence_id: seq_id,
1727 slot_id: 0,
1728 highest_slot_id: 0,
1729 cache_this: false,
1730 },
1731 rpc::Nfs4Op::PutFh { handle: dir_handle.to_vec() },
1732 ];
1733
1734 for name in filenames {
1735 ops.push(rpc::Nfs4Op::Lookup { name: name.to_string() });
1736 ops.push(rpc::Nfs4Op::GetAttr { attr_request });
1737 }
1738
1739 let msg = self.build_compound_msg(xid, "bstat", &ops)?;
1740 let reply_data = self.rpc_call(xid, &msg)?;
1741 let reply_data = self.unwrap_reply(reply_data)?;
1742 let reply = rpc::parse_compound_reply(&reply_data)?;
1743
1744 let mut results = Vec::new();
1746 for (i, name) in filenames.iter().enumerate() {
1747 let lookup_idx = 2 + i * 2;
1749 let getattr_idx = lookup_idx + 1;
1750
1751 if let Some(lookup_result) = reply.op_results.get(lookup_idx) {
1753 if lookup_result.status != rpc::NFS4_OK {
1754 continue; }
1756 } else {
1757 break; }
1759
1760 if let Some(getattr_result) = reply.op_results.get(getattr_idx) {
1762 if getattr_result.status == rpc::NFS4_OK
1763 && let (Some(size), Some((mtime_s, mtime_ns))) = (getattr_result.size, getattr_result.mtime) {
1764 results.push((name.to_string(), size, mtime_s, mtime_ns));
1765 }
1766 } else {
1767 break;
1768 }
1769 }
1770
1771 Ok(results)
1772 }
1773
1774 pub fn batch_read_head(
1782 &mut self,
1783 dir_handle: &[u8],
1784 filenames: &[&str],
1785 read_bytes: usize,
1786 ) -> Result<Vec<(String, Vec<u8>)>, NfsError> {
1787 if filenames.is_empty() {
1788 return Ok(Vec::new());
1789 }
1790
1791 let xid = self.next_xid();
1792 let seq_id = self.next_sequence_id();
1793
1794 let mut ops = Vec::with_capacity(2 + filenames.len() * 2);
1795 ops.push(rpc::Nfs4Op::Sequence {
1796 session_id: self.session_id,
1797 sequence_id: seq_id,
1798 slot_id: 0,
1799 highest_slot_id: 0,
1800 cache_this: false,
1801 });
1802 ops.push(rpc::Nfs4Op::PutFh { handle: dir_handle.to_vec() });
1803
1804 for name in filenames {
1805 ops.push(rpc::Nfs4Op::Lookup { name: name.to_string() });
1806 ops.push(rpc::Nfs4Op::Read {
1807 stateid: rpc::StateId::default(),
1808 offset: 0,
1809 count: read_bytes as u32,
1810 });
1811 }
1812
1813 let msg = self.build_compound_msg(xid, "brdh", &ops)?;
1814 let reply_data = self.rpc_call(xid, &msg)?;
1815 let reply_data = self.unwrap_reply(reply_data)?;
1816 let reply = rpc::parse_compound_reply(&reply_data)?;
1817
1818 let mut out = Vec::new();
1819 for (i, name) in filenames.iter().enumerate() {
1820 let lookup_idx = 2 + i * 2;
1821 let read_idx = lookup_idx + 1;
1822
1823 if let Some(lookup_result) = reply.op_results.get(lookup_idx) {
1825 if lookup_result.status != rpc::NFS4_OK {
1826 continue; }
1828 } else {
1829 break; }
1831
1832 if let Some(read_result) = reply.op_results.get(read_idx) {
1834 if read_result.status == rpc::NFS4_OK
1835 && let Some(ref data) = read_result.data {
1836 out.push((name.to_string(), data.clone()));
1837 }
1838 } else {
1839 break;
1840 }
1841 }
1842
1843 Ok(out)
1844 }
1845}
1846
1847pub struct NfsClientPool {
1853 clients: Vec<parking_lot::Mutex<NfsCompoundClient>>,
1854}
1855
1856impl NfsClientPool {
1857 pub fn new(info: &NfsBypassInfo, pool_size: usize) -> Result<Self, NfsError> {
1859 let pool_size = pool_size.max(1);
1860 let mut clients = Vec::with_capacity(pool_size);
1861 for i in 0..pool_size {
1862 match NfsCompoundClient::connect(info) {
1863 Ok(client) => {
1864 clients.push(parking_lot::Mutex::new(client));
1865 #[cfg(feature = "tls")]
1866 {
1867 use super::NfsTransportSecurity;
1868 if info.transport_security != NfsTransportSecurity::None {
1869 info!("NFS pool: session {} TLS active", i);
1870 }
1871 }
1872 }
1873 Err(e) => {
1874 if i == 0 {
1875 return Err(e); }
1877 info!("NFS pool: session {} failed ({}), pool size = {}", i, e, clients.len());
1878 break; }
1880 }
1881 }
1882 info!("NFS pool: {} sessions established to {}", clients.len(), info.server_addr);
1883 Ok(Self { clients })
1884 }
1885
1886 pub fn get(&self, idx: usize) -> &parking_lot::Mutex<NfsCompoundClient> {
1888 &self.clients[idx % self.clients.len()]
1889 }
1890
1891 pub fn len(&self) -> usize {
1893 self.clients.len()
1894 }
1895
1896 pub fn bypass_info(&self) -> NfsBypassInfo {
1898 self.clients
1899 .first()
1900 .map(|s| s.lock().server_info.clone())
1901 .unwrap_or_default()
1902 }
1903}
1904
1905pub fn probe_server_alive(server_addr: &std::net::SocketAddr) -> bool {
1911 use std::io::{Read, Write};
1912 let mut stream = match std::net::TcpStream::connect_timeout(server_addr, std::time::Duration::from_secs(constants::NFS_PROBE_TIMEOUT_SECS)) {
1913 Ok(s) => s,
1914 Err(_) => return false,
1915 };
1916 let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(constants::NFS_PROBE_TIMEOUT_SECS)));
1917 let _ = stream.set_write_timeout(Some(std::time::Duration::from_secs(constants::NFS_PROBE_TIMEOUT_SECS)));
1918 let msg = rpc::frame_rpc_message(&rpc::build_null_call(1));
1919 if stream.write_all(&msg).is_err() { return false; }
1920 if stream.flush().is_err() { return false; }
1921 let mut rm_buf = [0u8; 4];
1923 stream.read_exact(&mut rm_buf).is_ok()
1924}
1925
1926fn encode_auth_sys(enc: &mut super::xdr::XdrEncoder, uid: u32, gid: u32, machine: &str) {
1928 enc.encode_u32(rpc::AUTH_SYS);
1929 let mut body = super::xdr::XdrEncoder::new(64);
1930 body.encode_u32(0); body.encode_string(machine);
1932 body.encode_u32(uid);
1933 body.encode_u32(gid);
1934 body.encode_u32(1); body.encode_u32(gid); let body_bytes = body.into_bytes();
1937 enc.encode_opaque(&body_bytes);
1938}
1939
1940#[cfg(test)]
1941mod tests {
1942 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
1943 #[cfg(feature = "tls")]
1944 #[test]
1945 fn tls_alpn_is_sunrpc() {
1946 assert_eq!(crate::constants::NFS_TLS_ALPN, b"sunrpc");
1948 assert_eq!(crate::constants::NFS_TLS_ALPN.len(), 6);
1949 }
1950}