1pub mod actions;
11pub mod auth;
12pub mod wire;
13
14use anyhow::{Context, Result};
15use std::collections::{HashMap, VecDeque};
16use std::net::SocketAddr;
17use std::sync::Arc;
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::{TcpListener, TcpStream};
20use tokio::sync::{mpsc, Mutex};
21use tracing::{debug, error, info, trace, warn};
22
23use crate::llm::action_helper::call_llm;
24use crate::llm::ollama_client::OllamaClient;
25use crate::protocol::Event;
26use crate::server::connection::ConnectionId;
27use crate::server::SmbProtocol;
28use crate::state::app_state::AppState;
29use crate::state::server::{
30 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
31};
32use crate::state::ServerId;
33
34use crate::logging::emit::Log;
35use actions::SMB_OPERATION_EVENT;
36use wire::{command, status, FileMeta, RequestHeader, ResponseHeader, HEADER_LEN};
37
38const FIRST_MESSAGE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
45
46const IDLE_BETWEEN_MESSAGES_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(900);
54
55const BODY_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct Deadlines {
71 pub first_message: std::time::Duration,
73 pub idle: std::time::Duration,
75 pub body: std::time::Duration,
77}
78
79impl Default for Deadlines {
80 fn default() -> Self {
81 Self {
82 first_message: FIRST_MESSAGE_READ_TIMEOUT,
83 idle: IDLE_BETWEEN_MESSAGES_TIMEOUT,
84 body: BODY_READ_TIMEOUT,
85 }
86 }
87}
88
89pub const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
91
92pub const MAX_SESSIONS_PER_CONNECTION: usize = 16;
104
105pub const MAX_TREES_PER_CONNECTION: usize = 64;
113
114pub const MAX_OPEN_FILES_PER_CONNECTION: usize = 1024;
123
124const CONNECTION_CAP_REFUSAL: &[u8] = b"";
132
133pub const MAX_WRITE_SIZE: u32 = 1024 * 1024;
137
138pub const MAX_MESSAGE_BYTES: usize = MAX_WRITE_SIZE as usize + 64 * 1024;
148
149pub const MAX_COMPOUND_REQUESTS: usize = 32;
158
159pub const MAX_RESPONSE_FRAME_BYTES: usize = wire::NBSS_MAX_LEN - 2 * 1024 * 1024;
169
170pub const MAX_READ_SIZE: u32 = 1024 * 1024;
174
175pub const MAX_TRANSACT_SIZE: u32 = 1024 * 1024;
179
180const SERVER_GUID: [u8; 16] = [
182 0x4e, 0x65, 0x74, 0x47, 0x65, 0x74, 0x53, 0x4d, 0x42, 0x32, 0x2d, 0x73, 0x72, 0x76, 0x01, 0x00,
183];
184
185const STATUS_NOT_A_DIRECTORY: u32 = 0xC000_0103;
187const STATUS_FILE_IS_A_DIRECTORY: u32 = 0xC000_00BA;
189const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
191
192const FILE_CREATE: u32 = 0x0000_0002;
194const FILE_DIRECTORY_FILE: u32 = 0x0000_0001;
195const FILE_NON_DIRECTORY_FILE: u32 = 0x0000_0040;
196const FILE_DELETE_ON_CLOSE: u32 = 0x0000_1000;
197
198const RESTART_SCANS: u8 = 0x01;
200const RETURN_SINGLE_ENTRY: u8 = 0x02;
201const REOPEN: u8 = 0x10;
202
203const DIALECT_SMB_2_1: u16 = 0x0210;
205const DIALECT_SMB_2_0_2: u16 = 0x0202;
206
207pub struct SmbServer;
209
210#[derive(Debug, Clone)]
213struct SmbSession {
214 username: String,
215 authenticated: bool,
216}
217
218#[derive(Debug, Clone)]
220struct SmbTreeConnect {
221 session_id: u64,
222 share_name: String,
223 is_pipe: bool,
224}
225
226#[derive(Debug, Clone)]
228struct SmbFileHandle {
229 path: String,
230 tree_id: u32,
231 meta: FileMeta,
232 size_known: bool,
235 listing: Option<VecDeque<(String, FileMeta)>>,
237}
238
239struct SmbConnectionState {
241 sessions: HashMap<u64, SmbSession>,
242 trees: HashMap<u32, SmbTreeConnect>,
243 files: HashMap<[u8; 16], SmbFileHandle>,
244 next_session_id: u64,
245 next_tree_id: u32,
246 next_file_index: u64,
247}
248
249impl SmbConnectionState {
250 fn new() -> Self {
251 Self {
252 sessions: HashMap::new(),
253 trees: HashMap::new(),
254 files: HashMap::new(),
255 next_session_id: 1,
256 next_tree_id: 1,
257 next_file_index: 1,
258 }
259 }
260
261 fn has_authenticated_session(&self) -> bool {
262 self.sessions.values().any(|s| s.authenticated)
263 }
264
265 fn allocate_session(&mut self, username: String, authenticated: bool) -> u64 {
266 let sid = self.next_session_id;
267 self.next_session_id += 1;
268 self.sessions.insert(
269 sid,
270 SmbSession {
271 username,
272 authenticated,
273 },
274 );
275 sid
276 }
277}
278
279struct Ctx<'a> {
281 llm_client: &'a OllamaClient,
282 app_state: &'a Arc<AppState>,
283 server_id: ServerId,
284 connection_id: ConnectionId,
285 protocol: &'a Arc<SmbProtocol>,
286 state: &'a Mutex<SmbConnectionState>,
287 status_tx: &'a mpsc::UnboundedSender<String>,
288 deadlines: Deadlines,
289}
290
291#[derive(Debug, Default, Clone, Copy)]
294struct Chain {
295 session_id: u64,
296 tree_id: u32,
297 file_id: Option<[u8; 16]>,
298}
299
300const RELATED_FILE_ID: [u8; 16] = [0xFF; 16];
302
303impl SmbServer {
304 pub async fn spawn_with_llm_actions(
306 listen_addr: SocketAddr,
307 llm_client: OllamaClient,
308 app_state: Arc<AppState>,
309 status_tx: mpsc::UnboundedSender<String>,
310 server_id: ServerId,
311 deadlines: Deadlines,
312 ) -> Result<SocketAddr> {
313 Log::new(Some(&status_tx)).info(format!(
314 "SMB server (LLM-controlled, guest-only) starting on {}",
315 listen_addr
316 ));
317
318 let protocol = Arc::new(SmbProtocol::new());
319
320 let listener = TcpListener::bind(listen_addr)
322 .await
323 .context("Failed to bind SMB TCP listener")?;
324
325 let actual_addr = listener.local_addr()?;
326
327 Log::new(Some(&status_tx)).info(format!("SMB server listening on {}", actual_addr));
328
329 let task_registrar = app_state.clone();
331 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
332 let accept_handle = tokio::spawn(async move {
333 info!("SMB server connection acceptor started");
334
335 loop {
336 trace!("SMB acceptor: waiting for connection");
337
338 match crate::server::accept_bounded::accept_bounded(
339 &listener,
340 &limiter,
341 CONNECTION_CAP_REFUSAL,
342 "SMB",
343 Some(&status_tx),
344 )
345 .await
346 {
347 Ok((stream, peer_addr, permit)) => {
348 Log::new(Some(&status_tx))
349 .info(format!("SMB connection accepted from {}", peer_addr));
350
351 let llm_client = llm_client.clone();
353 let app_state = app_state.clone();
354 let protocol = protocol.clone();
355 let status_tx = status_tx.clone();
356
357 let task_owner = app_state.clone();
359 task_owner
360 .spawn_server_task(server_id, async move {
361 let _permit = permit;
364 if let Err(e) = Self::handle_connection(
365 stream,
366 peer_addr,
367 llm_client,
368 app_state,
369 server_id,
370 protocol,
371 status_tx.clone(),
372 deadlines,
373 )
374 .await
375 {
376 Log::new(Some(&status_tx)).error(format!(
377 "SMB connection error from {}: {}",
378 peer_addr, e
379 ));
380 }
381 })
382 .await;
383 }
384 Err(e) => {
385 Log::new(Some(&status_tx)).error(format!("SMB accept error: {}", e));
386 }
387 }
388 }
389 });
390
391 task_registrar
393 .register_server_task(server_id, accept_handle)
394 .await;
395
396 Ok(actual_addr)
397 }
398
399 #[allow(clippy::too_many_arguments)]
401 async fn handle_connection(
402 stream: TcpStream,
403 peer_addr: SocketAddr,
404 llm_client: OllamaClient,
405 app_state: Arc<AppState>,
406 server_id: ServerId,
407 protocol: Arc<SmbProtocol>,
408 status_tx: mpsc::UnboundedSender<String>,
409 deadlines: Deadlines,
410 ) -> Result<()> {
411 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
413
414 Log::new(Some(&status_tx)).info(format!(
415 "SMB connection {} from {}",
416 connection_id, peer_addr
417 ));
418
419 let local_addr = stream
421 .local_addr()
422 .unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap());
423
424 let now = crate::utils::clock::Instant::now();
426 let conn_state = ServerConnectionState {
427 id: connection_id,
428 remote_addr: peer_addr,
429 local_addr,
430 bytes_sent: 0,
431 bytes_received: 0,
432 packets_sent: 0,
433 packets_received: 0,
434 last_activity: now,
435 status: ConnectionStatus::Active,
436 status_changed_at: now,
437 protocol_info: ProtocolConnectionInfo::empty(),
438 };
439
440 app_state
441 .add_connection_to_server(server_id, conn_state)
442 .await;
443 let _ = status_tx.send("__UPDATE_UI__".to_string());
444
445 let (reader, write_half) = tokio::io::split(stream);
450 let write_half = Arc::new(Mutex::new(write_half));
451 let mut reader = SmbReader::new(reader);
452
453 let peer_rx = crate::server::peer_support::register_peer_channel(
459 &app_state,
460 server_id,
461 connection_id.as_u32(),
462 )
463 .await;
464 crate::server::peer_support::spawn_peer_command_task(
465 peer_rx,
466 protocol.clone(),
467 app_state.clone(),
468 server_id,
469 connection_id.as_u32(),
470 write_half.clone(),
471 status_tx.clone(),
472 );
473
474 let ctx_state = Mutex::new(SmbConnectionState::new());
475 let ctx = Ctx {
476 llm_client: &llm_client,
477 app_state: &app_state,
478 server_id,
479 connection_id,
480 protocol: &protocol,
481 state: &ctx_state,
482 status_tx: &status_tx,
483 deadlines,
484 };
485 Self::run_smb_session(&mut reader, &write_half, peer_addr, &ctx).await;
486
487 app_state
492 .remove_peer_handle(server_id, connection_id.as_u32())
493 .await;
494 let _ = write_half.lock().await.shutdown().await;
495
496 app_state
498 .update_connection_status(server_id, connection_id, ConnectionStatus::Closed)
499 .await;
500 let _ = status_tx.send("__UPDATE_UI__".to_string());
501
502 Log::new(Some(&status_tx)).info(format!("SMB connection {} closed", connection_id));
503
504 Ok(())
505 }
506
507 async fn run_smb_session<R, W>(
516 reader: &mut SmbReader<R>,
517 write_half: &Arc<Mutex<W>>,
518 peer_addr: SocketAddr,
519 ctx: &Ctx<'_>,
520 ) where
521 R: tokio::io::AsyncRead + Unpin,
522 W: tokio::io::AsyncWrite + Unpin,
523 {
524 loop {
525 let header_timeout = if ctx.state.lock().await.has_authenticated_session() {
531 ctx.deadlines.idle
532 } else {
533 ctx.deadlines.first_message
534 };
535 let mut transport = [0u8; 4];
536 match tokio::time::timeout(header_timeout, reader.read_exact_counted(&mut transport))
537 .await
538 {
539 Err(_) => {
540 Log::new(Some(ctx.status_tx)).info(format!(
541 "SMB peer {} sent nothing for {}s; closing idle connection",
542 peer_addr,
543 header_timeout.as_secs()
544 ));
545 break;
546 }
547 Ok(Err(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
548 Log::new(Some(ctx.status_tx))
549 .info(format!("SMB client {} disconnected", peer_addr));
550 break;
551 }
552 Ok(Err(e)) => {
553 error!("SMB read error from {}: {}", peer_addr, e);
554 break;
555 }
556 Ok(Ok(_)) => {}
557 }
558 flush_read_stats(reader, ctx.app_state, ctx.server_id, ctx.connection_id).await;
559
560 let (kind, len) = wire::parse_frame_header(transport);
561 match kind {
562 wire::NBSS_SESSION_MESSAGE => {}
563 wire::NBSS_KEEPALIVE => continue,
564 wire::NBSS_SESSION_REQUEST if len <= 256 => {
565 let mut names = vec![0u8; len];
568 if read_body_exact(reader, &mut names, ctx.deadlines.body)
569 .await
570 .is_err()
571 {
572 break;
573 }
574 let positive = [wire::NBSS_POSITIVE_RESPONSE, 0, 0, 0];
575 if write_counted(write_half, &positive, ctx).await.is_err() {
576 break;
577 }
578 continue;
579 }
580 _ => {
581 Log::new(Some(ctx.status_tx)).warn(format!(
582 "SMB peer {} sent {:02x?}, which is not a Direct TCP session message \
583 (MS-SMB2 2.1: 0x00 and a 24-bit length); closing",
584 peer_addr, transport
585 ));
586 break;
587 }
588 }
589
590 if len < HEADER_LEN {
591 Log::new(Some(ctx.status_tx)).warn(format!(
592 "SMB peer {} framed a {}-byte message, shorter than an SMB2 header; closing",
593 peer_addr, len
594 ));
595 break;
596 }
597
598 if len > MAX_MESSAGE_BYTES {
599 let mut header = [0u8; HEADER_LEN];
603 if read_body_exact(reader, &mut header, ctx.deadlines.body)
604 .await
605 .is_ok()
606 {
607 if let Some(req) = RequestHeader::parse(&header) {
608 Log::new(Some(ctx.status_tx)).warn(format!(
609 "SMB2 command 0x{:04x} in a {} byte frame refused \
610 (decision=fail_closed_message_too_large, limit {}); replying \
611 STATUS_INVALID_PARAMETER and closing",
612 req.command, len, MAX_MESSAGE_BYTES
613 ));
614 let reply = wire::error_response(&ResponseHeader::for_request(
615 &req,
616 status::INVALID_PARAMETER,
617 ));
618 let _ = write_counted(write_half, &wire::frame(&reply), ctx).await;
619 }
620 }
621 flush_read_stats(reader, ctx.app_state, ctx.server_id, ctx.connection_id).await;
622 break;
623 }
624
625 let mut message = vec![0u8; len];
626 if let Err(e) = read_body_exact(reader, &mut message, ctx.deadlines.body).await {
627 debug!(
628 "SMB peer {} did not deliver its announced frame: {}",
629 peer_addr, e
630 );
631 break;
632 }
633 flush_read_stats(reader, ctx.app_state, ctx.server_id, ctx.connection_id).await;
634
635 let responses = match Self::process_frame(&message, peer_addr, ctx).await {
636 Some(responses) => responses,
637 None => break,
638 };
639 if responses.is_empty() {
640 continue;
641 }
642 let framed = wire::frame(&wire::chain(responses));
643 match write_counted(write_half, &framed, ctx).await {
644 Ok(()) => trace!(
645 "SMB2 response sent to {}, {} bytes",
646 peer_addr,
647 framed.len()
648 ),
649 Err(e) => {
650 error!("Failed to send SMB2 response to {}: {}", peer_addr, e);
651 break;
652 }
653 }
654 }
655 }
656
657 async fn process_frame(
662 frame: &[u8],
663 peer_addr: SocketAddr,
664 ctx: &Ctx<'_>,
665 ) -> Option<Vec<Vec<u8>>> {
666 let mut responses = Vec::new();
667 let mut chain = Chain::default();
668 let mut offset = 0usize;
669 let mut requests = 0usize;
670 let mut response_bytes = 0usize;
671
672 loop {
673 let rest = &frame[offset..];
674 let located = match wire::next_in_chain(rest) {
675 Ok(located) => located,
676 Err(wire::ChainError::NotSmb2) if offset == 0 => {
677 let what = if rest.starts_with(b"\xFFSMB") {
678 "an SMB1 message; this server speaks SMB2 only"
679 } else {
680 "not an SMB2 message"
681 };
682 Log::new(Some(ctx.status_tx)).warn(format!(
683 "Invalid SMB2 signature from {} ({})",
684 peer_addr, what
685 ));
686 return None;
687 }
688 Err(wire::ChainError::NotSmb2) => {
689 warn!(
690 "SMB2 compound chain from {} ends in a malformed request",
691 peer_addr
692 );
693 break;
694 }
695 Err(wire::ChainError::BadNextCommand(req)) => {
696 warn!(
697 "SMB2 NextCommand {} from {} does not point at a request; refusing",
698 req.next_command, peer_addr
699 );
700 responses.push(wire::error_response(&ResponseHeader::for_request(
701 &req,
702 status::INVALID_PARAMETER,
703 )));
704 break;
705 }
706 };
707 let mut req = located.header;
708 let message = located.message;
709
710 if req.is_related() && offset > 0 {
711 req.session_id = chain.session_id;
712 req.tree_id = chain.tree_id;
713 }
714 chain.session_id = req.session_id;
715 chain.tree_id = req.tree_id;
716
717 debug!("SMB2 command 0x{:04x} from {}", req.command, peer_addr);
718 requests += 1;
719 let response = if requests > MAX_COMPOUND_REQUESTS {
720 if requests == MAX_COMPOUND_REQUESTS + 1 {
721 Log::new(Some(ctx.status_tx)).warn(format!(
722 "SMB2 compound from {} carries more than {} requests \
723 (decision=fail_closed_compound_cap); answering the rest \
724 STATUS_INSUFFICIENT_RESOURCES without acting on them",
725 peer_addr, MAX_COMPOUND_REQUESTS
726 ));
727 }
728 Some(wire::error_response(&ResponseHeader::for_request(
729 &req,
730 status::INSUFFICIENT_RESOURCES,
731 )))
732 } else {
733 match Self::handle_request(&req, message, &mut chain, ctx).await {
734 Ok(response) => response,
735 Err(e) => {
736 error!("SMB2 command 0x{:04x} failed: {}", req.command, e);
737 Some(wire::error_response(&ResponseHeader::for_request(
738 &req,
739 status::INTERNAL_ERROR,
740 )))
741 }
742 }
743 };
744 if let Some(mut response) = response {
745 if response_bytes + response.len().div_ceil(8) * 8 > MAX_RESPONSE_FRAME_BYTES {
747 Log::new(Some(ctx.status_tx)).warn(format!(
748 "SMB2 command 0x{:04x} from {}: its {} byte response would take the \
749 compound reply past {} bytes (decision=fail_closed_response_too_large); \
750 replying STATUS_INSUFFICIENT_RESOURCES",
751 req.command,
752 peer_addr,
753 response.len(),
754 MAX_RESPONSE_FRAME_BYTES
755 ));
756 response = wire::error_response(&ResponseHeader::for_request(
757 &req,
758 status::INSUFFICIENT_RESOURCES,
759 ));
760 }
761 response_bytes += response.len().div_ceil(8) * 8;
762 responses.push(response);
763 }
764
765 match located.next {
766 Some(next) => offset += next,
767 None => break,
768 }
769 }
770 Some(responses)
771 }
772
773 async fn handle_request(
776 req: &RequestHeader,
777 message: &[u8],
778 chain: &mut Chain,
779 ctx: &Ctx<'_>,
780 ) -> Result<Option<Vec<u8>>> {
781 let body = &message[HEADER_LEN..];
782 let error = |code: u32| -> Result<Option<Vec<u8>>> {
783 Ok(Some(wire::error_response(&ResponseHeader::for_request(
784 req, code,
785 ))))
786 };
787
788 let exempt = matches!(
797 req.command,
798 command::NEGOTIATE | command::SESSION_SETUP | command::ECHO | command::CANCEL
799 );
800 if !exempt {
801 let admitted = ctx
802 .state
803 .lock()
804 .await
805 .sessions
806 .get(&req.session_id)
807 .is_some_and(|s| s.authenticated);
808 if !admitted {
809 Log::new(Some(ctx.status_tx)).warn(format!(
810 "SMB2 command 0x{:04x} refused (decision=fail_closed_no_session): session \
811 {} is not an authenticated session on this connection; replying \
812 STATUS_USER_SESSION_DELETED",
813 req.command, req.session_id
814 ));
815 return error(status::USER_SESSION_DELETED);
816 }
817 }
818
819 let needs_tree = !matches!(
821 req.command,
822 command::NEGOTIATE
823 | command::SESSION_SETUP
824 | command::LOGOFF
825 | command::TREE_CONNECT
826 | command::ECHO
827 | command::CANCEL
828 );
829 if needs_tree {
830 let connected = ctx
831 .state
832 .lock()
833 .await
834 .trees
835 .get(&req.tree_id)
836 .is_some_and(|t| t.session_id == req.session_id);
837 if !connected {
838 Log::new(Some(ctx.status_tx)).warn(format!(
839 "SMB2 command 0x{:04x} refused: tree {} is not connected on session {}; \
840 replying STATUS_NETWORK_NAME_DELETED",
841 req.command, req.tree_id, req.session_id
842 ));
843 return error(status::NETWORK_NAME_DELETED);
844 }
845 }
846
847 match req.command {
848 command::NEGOTIATE => Self::negotiate(req, body, ctx),
849 command::SESSION_SETUP => Self::session_setup(req, message, ctx).await,
850 command::LOGOFF => {
851 let mut s = ctx.state.lock().await;
852 if let Some(session) = s.sessions.remove(&req.session_id) {
853 debug!(
854 "SMB2 LOGOFF: session {} ({:?})",
855 req.session_id, session.username
856 );
857 }
858 s.trees.retain(|_, t| t.session_id != req.session_id);
859 let trees: std::collections::HashSet<u32> = s.trees.keys().copied().collect();
861 s.files.retain(|_, f| trees.contains(&f.tree_id));
862 Ok(Some(wire::empty_response(&ResponseHeader::for_request(
863 req,
864 status::SUCCESS,
865 ))))
866 }
867 command::TREE_CONNECT => Self::tree_connect(req, message, ctx).await,
868 command::TREE_DISCONNECT => {
869 let mut s = ctx.state.lock().await;
870 s.trees.remove(&req.tree_id);
871 s.files.retain(|_, f| f.tree_id != req.tree_id);
872 Ok(Some(wire::empty_response(&ResponseHeader::for_request(
873 req,
874 status::SUCCESS,
875 ))))
876 }
877 command::CREATE => Self::create(req, message, chain, ctx).await,
878 command::CLOSE => Self::close(req, body, chain, ctx).await,
879 command::FLUSH => {
880 let Some(file_id) = wire::parse_flush(body) else {
881 return error(status::INVALID_PARAMETER);
882 };
883 if Self::handle(ctx, req, file_id, chain).await.is_none() {
884 return error(status::FILE_CLOSED);
885 }
886 Ok(Some(wire::empty_response(&ResponseHeader::for_request(
887 req,
888 status::SUCCESS,
889 ))))
890 }
891 command::READ => Self::read(req, body, chain, ctx).await,
892 command::WRITE => Self::write(req, message, chain, ctx).await,
893 command::QUERY_INFO => Self::query_info(req, body, chain, ctx).await,
894 command::QUERY_DIRECTORY => Self::query_directory(req, message, chain, ctx).await,
895 command::ECHO => Ok(Some(wire::empty_response(&ResponseHeader::for_request(
896 req,
897 status::SUCCESS,
898 )))),
899 command::CANCEL => Ok(None),
901 command::IOCTL => {
902 debug!("SMB2 IOCTL refused: no FSCTL is implemented");
905 error(status::INVALID_DEVICE_REQUEST)
906 }
907 other => {
908 Log::new(Some(ctx.status_tx)).warn(format!(
909 "Unsupported SMB2 command 0x{:04x}; replying STATUS_NOT_SUPPORTED",
910 other
911 ));
912 error(status::NOT_SUPPORTED)
913 }
914 }
915 }
916
917 fn negotiate(req: &RequestHeader, body: &[u8], ctx: &Ctx<'_>) -> Result<Option<Vec<u8>>> {
920 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
921 let Some(offered) = wire::parse_negotiate(body) else {
922 return Ok(Some(wire::error_response(
923 &hdr.with_status(status::INVALID_PARAMETER),
924 )));
925 };
926 let Some(dialect) = [DIALECT_SMB_2_1, DIALECT_SMB_2_0_2]
927 .into_iter()
928 .find(|d| offered.contains(d))
929 else {
930 Log::new(Some(ctx.status_tx)).warn(format!(
931 "SMB2 NEGOTIATE offered no dialect this server speaks ({:04x?}); it speaks \
932 0x0210 and 0x0202",
933 offered
934 ));
935 return Ok(Some(wire::error_response(
936 &hdr.with_status(status::NOT_SUPPORTED),
937 )));
938 };
939 Log::new(Some(ctx.status_tx)).debug(format!(
940 "SMB2 NEGOTIATE: choosing dialect 0x{:04x}",
941 dialect
942 ));
943
944 let params = wire::NegotiateParams {
945 dialect,
946 server_guid: SERVER_GUID,
947 capabilities: 0,
950 max_transact_size: MAX_TRANSACT_SIZE,
951 max_read_size: MAX_READ_SIZE,
952 max_write_size: MAX_WRITE_SIZE,
954 system_time: filetime_now(),
955 security_blob: auth::negotiate_blob(),
956 };
957 Ok(Some(wire::negotiate_response(&hdr, ¶ms)))
958 }
959
960 async fn session_setup(
967 req: &RequestHeader,
968 message: &[u8],
969 ctx: &Ctx<'_>,
970 ) -> Result<Option<Vec<u8>>> {
971 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
972 let Some(blob) = wire::parse_session_setup(message) else {
973 return Ok(Some(wire::error_response(
974 &hdr.with_status(status::INVALID_PARAMETER),
975 )));
976 };
977
978 if blob.is_empty() {
979 if let Some(refusal) = Self::session_cap_refusal(req, ctx).await {
983 return Ok(Some(refusal));
984 }
985 return Self::decide_login(
986 req,
987 None,
988 "guest",
989 "",
990 "guest",
991 false,
992 auth::Wrapping::Raw,
993 ctx,
994 )
995 .await;
996 }
997
998 let Some((token, wrapping)) = auth::find_ntlmssp(blob) else {
999 Log::new(Some(ctx.status_tx)).warn(
1000 "SMB2 SESSION_SETUP refused (decision=fail_closed_unsupported_mechanism): the \
1001 security buffer carries no NTLMSSP token, and NTLMSSP is the only mechanism \
1002 this server offers; replying STATUS_LOGON_FAILURE",
1003 );
1004 return Ok(Some(wire::error_response(
1005 &hdr.with_status(status::LOGON_FAILURE),
1006 )));
1007 };
1008
1009 match auth::message_type(token) {
1010 Some(auth::NTLMSSP_NEGOTIATE) => {
1011 let restarted = {
1015 let s = ctx.state.lock().await;
1016 s.sessions
1017 .get(&req.session_id)
1018 .is_some_and(|s| !s.authenticated)
1019 .then_some(req.session_id)
1020 };
1021 let session_id = match restarted {
1022 Some(sid) => sid,
1023 None => {
1024 if let Some(refusal) = Self::session_cap_refusal(req, ctx).await {
1025 return Ok(Some(refusal));
1026 }
1027 ctx.state
1028 .lock()
1029 .await
1030 .allocate_session(String::new(), false)
1031 }
1032 };
1033 let challenge = auth::challenge(
1034 auth::negotiate_flags(token),
1035 server_challenge(),
1036 filetime_now(),
1037 );
1038 debug!(
1039 "SMB2 SESSION_SETUP: NTLMSSP NEGOTIATE on new session {}; sending CHALLENGE",
1040 session_id
1041 );
1042 let hdr = hdr
1043 .with_status(status::MORE_PROCESSING_REQUIRED)
1044 .with_session_id(session_id);
1045 Ok(Some(wire::session_setup_response(
1046 &hdr,
1047 0,
1048 &auth::wrap_challenge(&challenge, wrapping),
1049 )))
1050 }
1051 Some(auth::NTLMSSP_AUTHENTICATE) => {
1052 let pending = ctx
1053 .state
1054 .lock()
1055 .await
1056 .sessions
1057 .get(&req.session_id)
1058 .is_some_and(|s| !s.authenticated);
1059 if !pending {
1060 Log::new(Some(ctx.status_tx)).warn(format!(
1061 "SMB2 SESSION_SETUP: NTLMSSP AUTHENTICATE for session {}, which has no \
1062 exchange in progress; replying STATUS_USER_SESSION_DELETED",
1063 req.session_id
1064 ));
1065 return Ok(Some(wire::error_response(
1066 &hdr.with_status(status::USER_SESSION_DELETED),
1067 )));
1068 }
1069 let Some(who) = auth::parse_authenticate(token) else {
1070 ctx.state.lock().await.sessions.remove(&req.session_id);
1071 return Ok(Some(wire::error_response(
1072 &hdr.with_status(status::INVALID_PARAMETER),
1073 )));
1074 };
1075 let auth_type = if who.anonymous { "anonymous" } else { "ntlm" };
1076 Self::decide_login(
1077 req,
1078 Some(req.session_id),
1079 &who.user,
1080 &who.domain,
1081 auth_type,
1082 who.anonymous,
1083 wrapping,
1084 ctx,
1085 )
1086 .await
1087 }
1088 _ => Ok(Some(wire::error_response(
1089 &hdr.with_status(status::INVALID_PARAMETER),
1090 ))),
1091 }
1092 }
1093
1094 #[allow(clippy::too_many_arguments)]
1097 async fn decide_login(
1098 req: &RequestHeader,
1099 pending: Option<u64>,
1100 username: &str,
1101 domain: &str,
1102 auth_type: &str,
1103 anonymous: bool,
1104 wrapping: auth::Wrapping,
1105 ctx: &Ctx<'_>,
1106 ) -> Result<Option<Vec<u8>>> {
1107 Log::new(Some(ctx.status_tx)).info(format!(
1108 "SMB2 SESSION_SETUP for user: {:?} ({})",
1109 username, auth_type
1110 ));
1111
1112 let mut params = serde_json::json!({
1113 "username": username,
1114 "auth_type": auth_type,
1115 });
1116 if !domain.is_empty() {
1117 params["domain"] = serde_json::json!(domain);
1118 }
1119 if auth_type == "ntlm" {
1120 params["password_verified"] = serde_json::json!(false);
1122 }
1123
1124 let actions = match Self::consult_llm(ctx, "session_setup", params).await {
1125 Ok(actions) => actions,
1126 Err(e) => {
1127 Log::new(Some(ctx.status_tx)).warn(format!(
1128 "LLM error during SMB authentication for user {:?} \
1129 (decision=fail_closed_llm_error) - denying auth: {}",
1130 username, e
1131 ));
1132 return Self::deny_login(req, pending, ctx).await;
1133 }
1134 };
1135
1136 let auth_allowed = actions
1138 .iter()
1139 .any(|a| a.get("type").and_then(|t| t.as_str()) == Some("smb_auth_success"));
1140
1141 if !auth_allowed {
1142 let decision = if actions.is_empty() {
1145 "fail_closed_no_action"
1146 } else {
1147 "model_reject"
1148 };
1149 Log::new(Some(ctx.status_tx)).warn(format!(
1150 "SMB authentication denied for user {:?} (decision={}); replying \
1151 STATUS_ACCESS_DENIED",
1152 username, decision
1153 ));
1154 return Self::deny_login(req, pending, ctx).await;
1155 }
1156
1157 let session_id = {
1158 let mut s = ctx.state.lock().await;
1159 match pending {
1160 Some(sid) => {
1161 if let Some(session) = s.sessions.get_mut(&sid) {
1162 session.username = username.to_string();
1163 session.authenticated = true;
1164 }
1165 sid
1166 }
1167 None => s.allocate_session(username.to_string(), true),
1168 }
1169 };
1170 Log::new(Some(ctx.status_tx)).info(format!(
1171 "SMB authentication successful for user: {:?} (session {})",
1172 username, session_id
1173 ));
1174 let _ = ctx.status_tx.send("__UPDATE_UI__".to_string());
1175
1176 let flags = if anonymous {
1180 wire::SESSION_FLAG_IS_NULL
1181 } else {
1182 wire::SESSION_FLAG_IS_GUEST
1183 };
1184 let hdr = ResponseHeader::for_request(req, status::SUCCESS).with_session_id(session_id);
1185 Ok(Some(wire::session_setup_response(
1186 &hdr,
1187 flags,
1188 &auth::accept_completed(wrapping),
1189 )))
1190 }
1191
1192 async fn deny_login(
1194 req: &RequestHeader,
1195 pending: Option<u64>,
1196 ctx: &Ctx<'_>,
1197 ) -> Result<Option<Vec<u8>>> {
1198 if let Some(sid) = pending {
1199 ctx.state.lock().await.sessions.remove(&sid);
1200 }
1201 let hdr = ResponseHeader::for_request(req, status::ACCESS_DENIED);
1202 Ok(Some(wire::session_setup_response(&hdr, 0, &[])))
1203 }
1204
1205 async fn session_cap_refusal(req: &RequestHeader, ctx: &Ctx<'_>) -> Option<Vec<u8>> {
1208 let (held, pending) = {
1209 let s = ctx.state.lock().await;
1210 let pending = s.sessions.values().filter(|s| !s.authenticated).count();
1211 (s.sessions.len(), pending)
1212 };
1213 if held < MAX_SESSIONS_PER_CONNECTION {
1214 return None;
1215 }
1216 Log::new(Some(ctx.status_tx)).warn(format!(
1217 "SMB2 SESSION_SETUP refused (decision=fail_closed_session_cap): this connection \
1218 already holds {} sessions ({} still mid-exchange), the most one connection may \
1219 hold is {}; replying STATUS_INSUFFICIENT_RESOURCES",
1220 held, pending, MAX_SESSIONS_PER_CONNECTION
1221 ));
1222 Some(wire::error_response(&ResponseHeader::for_request(
1223 req,
1224 status::INSUFFICIENT_RESOURCES,
1225 )))
1226 }
1227
1228 async fn tree_connect(
1233 req: &RequestHeader,
1234 message: &[u8],
1235 ctx: &Ctx<'_>,
1236 ) -> Result<Option<Vec<u8>>> {
1237 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1238 let Some(path) = wire::parse_tree_connect(message) else {
1239 return Ok(Some(wire::error_response(
1240 &hdr.with_status(status::INVALID_PARAMETER),
1241 )));
1242 };
1243 let share = path.rsplit('\\').next().unwrap_or("").to_string();
1244 let is_pipe = share.eq_ignore_ascii_case("IPC$");
1245
1246 let tree_id = {
1247 let mut s = ctx.state.lock().await;
1248 if s.trees.len() >= MAX_TREES_PER_CONNECTION {
1251 let held = s.trees.len();
1252 drop(s);
1253 Log::new(Some(ctx.status_tx)).warn(format!(
1254 "SMB2 TREE_CONNECT {} refused (decision=fail_closed_tree_cap): this \
1255 connection already holds {} tree connects, the most one connection may \
1256 hold is {}; replying STATUS_INSUFFICIENT_RESOURCES",
1257 path, held, MAX_TREES_PER_CONNECTION
1258 ));
1259 return Ok(Some(wire::error_response(
1260 &hdr.with_status(status::INSUFFICIENT_RESOURCES),
1261 )));
1262 }
1263 let tid = s.next_tree_id;
1264 s.next_tree_id += 1;
1265 s.trees.insert(
1266 tid,
1267 SmbTreeConnect {
1268 session_id: req.session_id,
1269 share_name: share.clone(),
1270 is_pipe,
1271 },
1272 );
1273 tid
1274 };
1275 Log::new(Some(ctx.status_tx)).info(format!(
1276 "SMB2 TREE_CONNECT {} -> tree {} ({})",
1277 path,
1278 tree_id,
1279 if is_pipe { "pipe" } else { "disk" }
1280 ));
1281 let share_type = if is_pipe {
1282 wire::SHARE_TYPE_PIPE
1283 } else {
1284 wire::SHARE_TYPE_DISK
1285 };
1286 Ok(Some(wire::tree_connect_response(
1287 &hdr.with_tree_id(tree_id),
1288 share_type,
1289 0x001F_01FF,
1290 )))
1291 }
1292
1293 async fn handle(
1296 ctx: &Ctx<'_>,
1297 req: &RequestHeader,
1298 mut file_id: [u8; 16],
1299 chain: &Chain,
1300 ) -> Option<([u8; 16], SmbFileHandle)> {
1301 if req.is_related() && file_id == RELATED_FILE_ID {
1302 file_id = chain.file_id?;
1303 }
1304 let s = ctx.state.lock().await;
1305 let handle = s.files.get(&file_id)?;
1306 (handle.tree_id == req.tree_id).then(|| (file_id, handle.clone()))
1307 }
1308
1309 async fn create(
1311 req: &RequestHeader,
1312 message: &[u8],
1313 chain: &mut Chain,
1314 ctx: &Ctx<'_>,
1315 ) -> Result<Option<Vec<u8>>> {
1316 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1317 let Some(wire::CreateRequest {
1318 disposition,
1319 options,
1320 path,
1321 }) = wire::parse_create(message)
1322 else {
1323 return Ok(Some(wire::error_response(
1324 &hdr.with_status(status::INVALID_PARAMETER),
1325 )));
1326 };
1327 let (is_pipe, open) = {
1328 let s = ctx.state.lock().await;
1329 let is_pipe = s.trees.get(&req.tree_id).is_some_and(|t| t.is_pipe);
1330 (is_pipe, s.files.len())
1331 };
1332 if is_pipe {
1333 debug!("SMB2 CREATE on a pipe share refused: no named pipe is implemented");
1334 return Ok(Some(wire::error_response(
1335 &hdr.with_status(status::NOT_SUPPORTED),
1336 )));
1337 }
1338 if open >= MAX_OPEN_FILES_PER_CONNECTION {
1342 Log::new(Some(ctx.status_tx)).warn(format!(
1343 "SMB2 CREATE {} refused (decision=fail_closed_open_file_cap): this connection \
1344 already holds {} open handles, the most one connection may hold is {}; \
1345 replying STATUS_TOO_MANY_OPENED_FILES",
1346 path, open, MAX_OPEN_FILES_PER_CONNECTION
1347 ));
1348 return Ok(Some(wire::error_response(
1349 &hdr.with_status(status::TOO_MANY_OPENED_FILES),
1350 )));
1351 }
1352
1353 Log::new(Some(ctx.status_tx)).info(format!("SMB2 CREATE request for: {}", path));
1354
1355 let mut params = serde_json::json!({
1356 "path": path,
1357 "disposition": disposition_name(disposition),
1358 "directory_requested": options & FILE_DIRECTORY_FILE != 0,
1359 });
1360 if options & FILE_DELETE_ON_CLOSE != 0 {
1365 params["delete_on_close"] = serde_json::json!(true);
1366 }
1367 let actions = match Self::consult_llm(ctx, "create", params).await {
1368 Ok(actions) => actions,
1369 Err(e) => {
1370 return Ok(Some(Self::llm_failure_response(
1371 req, "CREATE", &path, &e, ctx,
1372 )))
1373 }
1374 };
1375
1376 let action_type =
1381 |a: &serde_json::Value| a.get("type").and_then(|t| t.as_str()).map(String::from);
1382 let dir_action = actions
1383 .iter()
1384 .find(|a| action_type(a).as_deref() == Some("smb_create_directory"));
1385 let file_action = actions
1386 .iter()
1387 .find(|a| action_type(a).as_deref() == Some("smb_create_file"));
1388 let is_directory = dir_action.is_some();
1389 if dir_action.is_none() && file_action.is_none() {
1390 let decision = if actions.is_empty() {
1391 "fail_closed_no_action"
1392 } else {
1393 "model_reject"
1394 };
1395 Log::new(Some(ctx.status_tx)).warn(format!(
1396 "SMB2 CREATE refused for {} (decision={}): no smb_create_file or \
1397 smb_create_directory in the answer; replying STATUS_ACCESS_DENIED",
1398 path, decision
1399 ));
1400 return Ok(Some(wire::error_response(
1401 &hdr.with_status(status::ACCESS_DENIED),
1402 )));
1403 }
1404 if is_directory && options & FILE_NON_DIRECTORY_FILE != 0 {
1405 return Ok(Some(wire::error_response(
1406 &hdr.with_status(STATUS_FILE_IS_A_DIRECTORY),
1407 )));
1408 }
1409 if !is_directory && options & FILE_DIRECTORY_FILE != 0 {
1410 return Ok(Some(wire::error_response(
1411 &hdr.with_status(STATUS_NOT_A_DIRECTORY),
1412 )));
1413 }
1414
1415 let declared_size = file_action
1418 .filter(|_| !is_directory)
1419 .and_then(|a| a.get("size"))
1420 .and_then(|v| v.as_u64());
1421 let (file_id, meta) = {
1422 let mut s = ctx.state.lock().await;
1423 let index = s.next_file_index;
1424 s.next_file_index += 1;
1425 let file_id = file_id_for(index);
1426 let meta = FileMeta {
1427 is_directory,
1428 size: declared_size.unwrap_or(0),
1429 time: 0,
1430 file_index: index,
1431 };
1432 s.files.insert(
1433 file_id,
1434 SmbFileHandle {
1435 path: path.clone(),
1436 tree_id: req.tree_id,
1437 meta,
1438 size_known: is_directory || declared_size.is_some(),
1439 listing: None,
1440 },
1441 );
1442 (file_id, meta)
1443 };
1444 chain.file_id = Some(file_id);
1445
1446 debug!(
1447 "SMB2 CREATE: allocated {} handle for {}",
1448 if is_directory { "directory" } else { "file" },
1449 path
1450 );
1451 let create_action = if disposition == FILE_CREATE {
1452 wire::FILE_CREATED
1453 } else {
1454 wire::FILE_OPENED
1455 };
1456 Ok(Some(wire::create_response(
1457 &hdr,
1458 &file_id,
1459 &meta,
1460 create_action,
1461 )))
1462 }
1463
1464 async fn close(
1466 req: &RequestHeader,
1467 body: &[u8],
1468 chain: &Chain,
1469 ctx: &Ctx<'_>,
1470 ) -> Result<Option<Vec<u8>>> {
1471 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1472 let Some((flags, file_id)) = wire::parse_close(body) else {
1473 return Ok(Some(wire::error_response(
1474 &hdr.with_status(status::INVALID_PARAMETER),
1475 )));
1476 };
1477 let Some((file_id, handle)) = Self::handle(ctx, req, file_id, chain).await else {
1478 Log::new(Some(ctx.status_tx)).warn("SMB2 CLOSE: unknown file handle");
1479 return Ok(Some(wire::error_response(
1480 &hdr.with_status(status::FILE_CLOSED),
1481 )));
1482 };
1483 ctx.state.lock().await.files.remove(&file_id);
1484 Log::new(Some(ctx.status_tx)).info(format!("SMB2 CLOSE: {}", handle.path));
1485 let meta = (flags & wire::CLOSE_FLAG_POSTQUERY_ATTRIB != 0).then_some(&handle.meta);
1486 Ok(Some(wire::close_response(&hdr, meta)))
1487 }
1488
1489 async fn read(
1492 req: &RequestHeader,
1493 body: &[u8],
1494 chain: &Chain,
1495 ctx: &Ctx<'_>,
1496 ) -> Result<Option<Vec<u8>>> {
1497 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1498 let Some(request) = wire::parse_read(body) else {
1499 return Ok(Some(wire::error_response(
1500 &hdr.with_status(status::INVALID_PARAMETER),
1501 )));
1502 };
1503 if request.length > MAX_READ_SIZE {
1504 Log::new(Some(ctx.status_tx)).warn(format!(
1505 "SMB2 READ of {} bytes refused (decision=fail_closed_read_too_large, \
1506 MaxReadSize {}); replying STATUS_INVALID_PARAMETER",
1507 request.length, MAX_READ_SIZE
1508 ));
1509 return Ok(Some(wire::error_response(
1510 &hdr.with_status(status::INVALID_PARAMETER),
1511 )));
1512 }
1513 let length = request.length;
1514 let offset = request.offset;
1515 let Some((_, handle)) = Self::handle(ctx, req, request.file_id, chain).await else {
1516 return Ok(Some(wire::error_response(
1517 &hdr.with_status(status::FILE_CLOSED),
1518 )));
1519 };
1520 let path = handle.path;
1521 if handle.meta.is_directory {
1522 return Ok(Some(wire::error_response(
1523 &hdr.with_status(status::INVALID_DEVICE_REQUEST),
1524 )));
1525 }
1526 Log::new(Some(ctx.status_tx)).info(format!(
1527 "SMB2 READ: {} (offset={}, length={})",
1528 path, offset, length
1529 ));
1530
1531 let actions = match Self::consult_llm(
1534 ctx,
1535 "read",
1536 serde_json::json!({ "path": path, "offset": offset, "length": length }),
1537 )
1538 .await
1539 {
1540 Ok(actions) => actions,
1541 Err(e) => {
1542 return Ok(Some(Self::llm_failure_response(
1543 req, "READ", &path, &e, ctx,
1544 )))
1545 }
1546 };
1547
1548 let Some(action) = actions
1551 .iter()
1552 .find(|a| a.get("type").and_then(|t| t.as_str()) == Some("smb_read_file"))
1553 else {
1554 let decision = if actions.is_empty() {
1557 "fail_closed_no_action"
1558 } else {
1559 "model_reject"
1560 };
1561 Log::new(Some(ctx.status_tx)).warn(format!(
1562 "SMB2 READ refused for {} (decision={}): no smb_read_file in the answer; \
1563 replying STATUS_ACCESS_DENIED",
1564 path, decision
1565 ));
1566 return Ok(Some(wire::error_response(
1567 &hdr.with_status(status::ACCESS_DENIED),
1568 )));
1569 };
1570 let payload = action
1571 .get("content")
1572 .and_then(|c| c.as_str())
1573 .unwrap_or_default();
1574 let encoding = action.get("encoding").and_then(|e| e.as_str());
1575 let content = match actions::decode_smb_payload(payload, encoding) {
1576 Ok(bytes) => bytes,
1577 Err(e) => {
1578 Log::new(Some(ctx.status_tx))
1580 .warn(format!("SMB read: {} - refusing with STATUS_DATA_ERROR", e));
1581 return Ok(Some(wire::error_response(
1582 &hdr.with_status(status::DATA_ERROR),
1583 )));
1584 }
1585 };
1586
1587 let len = content.len() as u64;
1589 if offset >= len && !(offset == 0 && length == 0) {
1590 debug!(
1591 "SMB2 READ at {} of a {} byte file: END_OF_FILE",
1592 offset, len
1593 );
1594 return Ok(Some(wire::error_response(
1595 &hdr.with_status(status::END_OF_FILE),
1596 )));
1597 }
1598 let start = offset as usize;
1599 let end = (offset.saturating_add(length as u64)).min(len) as usize;
1600 let data = &content[start..end];
1601 debug!("SMB2 READ: returning {} bytes of {}", data.len(), path);
1602 Ok(Some(wire::read_response(&hdr, data)))
1603 }
1604
1605 async fn write(
1607 req: &RequestHeader,
1608 message: &[u8],
1609 chain: &Chain,
1610 ctx: &Ctx<'_>,
1611 ) -> Result<Option<Vec<u8>>> {
1612 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1613 let Some(request) = wire::parse_write(message) else {
1614 return Ok(Some(wire::error_response(
1615 &hdr.with_status(status::INVALID_PARAMETER),
1616 )));
1617 };
1618 let length = request.length;
1619 let offset = request.offset;
1620
1621 if length > MAX_WRITE_SIZE {
1622 Log::new(Some(ctx.status_tx)).warn(format!(
1623 "SMB2 WRITE: refusing {} byte write (MaxWriteSize {}) \
1624 decision=fail_closed_write_too_large",
1625 length, MAX_WRITE_SIZE
1626 ));
1627 return Ok(Some(wire::error_response(
1628 &hdr.with_status(status::INVALID_PARAMETER),
1629 )));
1630 }
1631 let Some(data) = request.data else {
1632 Log::new(Some(ctx.status_tx)).warn(format!(
1633 "SMB2 WRITE declares {} bytes past the end of a {} byte message; replying \
1634 STATUS_INVALID_PARAMETER",
1635 length,
1636 message.len()
1637 ));
1638 return Ok(Some(wire::error_response(
1639 &hdr.with_status(status::INVALID_PARAMETER),
1640 )));
1641 };
1642
1643 let Some((_, handle)) = Self::handle(ctx, req, request.file_id, chain).await else {
1644 return Ok(Some(wire::error_response(
1645 &hdr.with_status(status::FILE_CLOSED),
1646 )));
1647 };
1648 let path = handle.path;
1649 Log::new(Some(ctx.status_tx)).info(format!(
1650 "SMB2 WRITE: {} (offset={}, length={})",
1651 path, offset, length
1652 ));
1653
1654 let (content, data_encoding) = actions::encode_smb_payload(data);
1657
1658 let actions = match Self::consult_llm(
1661 ctx,
1662 "write",
1663 serde_json::json!({
1664 "path": path,
1665 "offset": offset,
1666 "data": content,
1667 "encoding": data_encoding
1668 }),
1669 )
1670 .await
1671 {
1672 Ok(actions) => actions,
1673 Err(e) => {
1674 return Ok(Some(Self::llm_failure_response(
1675 req, "WRITE", &path, &e, ctx,
1676 )))
1677 }
1678 };
1679
1680 let Some(write_action) = actions
1681 .iter()
1682 .find(|a| a.get("type").and_then(|t| t.as_str()) == Some("smb_write_file"))
1683 else {
1684 let decision = if actions.is_empty() {
1685 "fail_closed_no_action"
1686 } else {
1687 "model_reject"
1688 };
1689 Log::new(Some(ctx.status_tx)).warn(format!(
1690 "SMB2 WRITE refused for {} (decision={}): no smb_write_file in the answer; \
1691 replying STATUS_ACCESS_DENIED",
1692 path, decision
1693 ));
1694 return Ok(Some(wire::error_response(
1695 &hdr.with_status(status::ACCESS_DENIED),
1696 )));
1697 };
1698
1699 let bytes_written = write_action
1701 .get("bytes_written")
1702 .and_then(|v| v.as_u64())
1703 .map(|v| v.min(length as u64) as u32)
1704 .unwrap_or(length);
1705 debug!(
1706 "SMB2 WRITE: accepted {} of {} bytes to {}",
1707 bytes_written, length, path
1708 );
1709 Ok(Some(wire::write_response(&hdr, bytes_written)))
1710 }
1711
1712 async fn query_info(
1715 req: &RequestHeader,
1716 body: &[u8],
1717 chain: &Chain,
1718 ctx: &Ctx<'_>,
1719 ) -> Result<Option<Vec<u8>>> {
1720 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1721 let Some(request) = wire::parse_query_info(body) else {
1722 warn!("SMB2 QUERY_INFO: invalid request size");
1723 return Ok(Some(wire::error_response(
1724 &hdr.with_status(status::INVALID_PARAMETER),
1725 )));
1726 };
1727 if request.output_len > MAX_TRANSACT_SIZE {
1728 return Ok(Some(Self::transact_too_large(req, request.output_len, ctx)));
1729 }
1730 let info_type = request.info_type;
1731 let class = request.class;
1732 let output_len = request.output_len as usize;
1733 let Some((file_id, handle)) = Self::handle(ctx, req, request.file_id, chain).await else {
1734 return Ok(Some(wire::error_response(
1735 &hdr.with_status(status::FILE_CLOSED),
1736 )));
1737 };
1738 let path = handle.path.clone();
1739
1740 let buffer = match info_type {
1741 wire::INFO_FILESYSTEM => {
1742 let share = ctx
1743 .state
1744 .lock()
1745 .await
1746 .trees
1747 .get(&req.tree_id)
1748 .map(|t| t.share_name.clone())
1749 .unwrap_or_default();
1750 match wire::fs_info(class, &share, 0) {
1751 Some(buffer) => buffer,
1752 None => {
1753 return Ok(Some(wire::error_response(
1754 &hdr.with_status(status::INVALID_INFO_CLASS),
1755 )))
1756 }
1757 }
1758 }
1759 wire::INFO_FILE => {
1760 let mut meta = handle.meta;
1761 if wire::file_class_needs_size(class) && !handle.size_known {
1762 Log::new(Some(ctx.status_tx)).info(format!("SMB2 QUERY_INFO: {}", path));
1763 let actions = match Self::consult_llm(
1764 ctx,
1765 "query_info",
1766 serde_json::json!({ "path": path }),
1767 )
1768 .await
1769 {
1770 Ok(actions) => actions,
1771 Err(e) => {
1772 return Ok(Some(Self::llm_failure_response(
1773 req,
1774 "QUERY_INFO",
1775 &path,
1776 &e,
1777 ctx,
1778 )))
1779 }
1780 };
1781 let info = actions.iter().find(|a| {
1783 a.get("type").and_then(|t| t.as_str()) == Some("smb_get_file_info")
1784 });
1785 let Some(size) = info.and_then(|a| a.get("size")).and_then(|s| s.as_u64())
1786 else {
1787 let decision = if actions.is_empty() {
1788 "fail_closed_no_action"
1789 } else {
1790 "model_reject"
1791 };
1792 Log::new(Some(ctx.status_tx)).warn(format!(
1793 "SMB2 QUERY_INFO refused for {} (decision={}): no \
1794 smb_get_file_info with a size in the answer; replying \
1795 STATUS_ACCESS_DENIED",
1796 path, decision
1797 ));
1798 return Ok(Some(wire::error_response(
1799 &hdr.with_status(status::ACCESS_DENIED),
1800 )));
1801 };
1802 meta.size = size;
1803 if let Some(time) = info
1804 .and_then(|a| a.get("modified_time"))
1805 .and_then(|t| t.as_str())
1806 .and_then(parse_time)
1807 {
1808 meta.time = time;
1809 }
1810 if let Some(h) = ctx.state.lock().await.files.get_mut(&file_id) {
1811 h.meta = meta;
1812 h.size_known = true;
1813 }
1814 }
1815 match wire::file_info(class, &meta) {
1816 Some(buffer) => buffer,
1817 None => {
1818 return Ok(Some(wire::error_response(
1819 &hdr.with_status(status::INVALID_INFO_CLASS),
1820 )))
1821 }
1822 }
1823 }
1824 _ => {
1825 return Ok(Some(wire::error_response(
1827 &hdr.with_status(status::NOT_SUPPORTED),
1828 )));
1829 }
1830 };
1831
1832 if buffer.len() > output_len {
1833 return Ok(Some(wire::error_response(
1834 &hdr.with_status(STATUS_INFO_LENGTH_MISMATCH),
1835 )));
1836 }
1837 Ok(Some(wire::query_info_response(&hdr, &buffer)))
1838 }
1839
1840 async fn query_directory(
1844 req: &RequestHeader,
1845 message: &[u8],
1846 chain: &Chain,
1847 ctx: &Ctx<'_>,
1848 ) -> Result<Option<Vec<u8>>> {
1849 let hdr = ResponseHeader::for_request(req, status::SUCCESS);
1850 let Some(wire::QueryDirectoryRequest {
1851 class,
1852 flags,
1853 file_id,
1854 pattern,
1855 output_len,
1856 }) = wire::parse_query_directory(message)
1857 else {
1858 warn!("SMB2 QUERY_DIRECTORY: invalid request");
1859 return Ok(Some(wire::error_response(
1860 &hdr.with_status(status::INVALID_PARAMETER),
1861 )));
1862 };
1863 if output_len > MAX_TRANSACT_SIZE {
1864 return Ok(Some(Self::transact_too_large(req, output_len, ctx)));
1865 }
1866 let output_len = output_len as usize;
1867 if !wire::directory_class_supported(class) {
1868 return Ok(Some(wire::error_response(
1869 &hdr.with_status(status::INVALID_INFO_CLASS),
1870 )));
1871 }
1872 let Some((file_id, handle)) = Self::handle(ctx, req, file_id, chain).await else {
1873 return Ok(Some(wire::error_response(
1874 &hdr.with_status(status::FILE_CLOSED),
1875 )));
1876 };
1877 if !handle.meta.is_directory {
1878 return Ok(Some(wire::error_response(
1879 &hdr.with_status(status::INVALID_PARAMETER),
1880 )));
1881 }
1882 let path = handle.path.clone();
1883
1884 let restart = flags & (RESTART_SCANS | REOPEN) != 0 || handle.listing.is_none();
1885 if restart {
1886 Log::new(Some(ctx.status_tx))
1887 .info(format!("SMB2 QUERY_DIRECTORY: {} ({})", path, pattern));
1888 let actions = match Self::consult_llm(
1889 ctx,
1890 "query_directory",
1891 serde_json::json!({ "path": path, "pattern": pattern }),
1892 )
1893 .await
1894 {
1895 Ok(actions) => actions,
1896 Err(e) => {
1897 return Ok(Some(Self::llm_failure_response(
1898 req,
1899 "QUERY_DIRECTORY",
1900 &path,
1901 &e,
1902 ctx,
1903 )))
1904 }
1905 };
1906
1907 let Some(files) = actions
1911 .iter()
1912 .find(|a| a.get("type").and_then(|t| t.as_str()) == Some("smb_list_directory"))
1913 .and_then(|a| a.get("files"))
1914 .and_then(|f| f.as_array())
1915 .cloned()
1916 else {
1917 let decision = if actions.is_empty() {
1918 "fail_closed_no_action"
1919 } else {
1920 "model_reject"
1921 };
1922 Log::new(Some(ctx.status_tx)).warn(format!(
1923 "SMB2 QUERY_DIRECTORY refused for {} (decision={}): no smb_list_directory \
1924 in the answer; replying STATUS_ACCESS_DENIED",
1925 path, decision
1926 ));
1927 return Ok(Some(wire::error_response(
1928 &hdr.with_status(status::ACCESS_DENIED),
1929 )));
1930 };
1931
1932 let mut entries = VecDeque::new();
1933 let mut s = ctx.state.lock().await;
1934 let mut next_index = || {
1935 let i = s.next_file_index;
1936 s.next_file_index += 1;
1937 i
1938 };
1939 for dot in [".", ".."] {
1940 if wildcard_match(&pattern, dot) {
1941 let meta = FileMeta {
1942 is_directory: true,
1943 file_index: next_index(),
1944 ..FileMeta::default()
1945 };
1946 entries.push_back((dot.to_string(), meta));
1947 }
1948 }
1949 for file in &files {
1950 let Some(name) = file.get("name").and_then(|n| n.as_str()) else {
1951 continue;
1952 };
1953 let name = name.rsplit(['/', '\\']).next().unwrap_or(name);
1955 if name.is_empty() || name == "." || name == ".." || !wildcard_match(&pattern, name)
1956 {
1957 continue;
1958 }
1959 let meta = FileMeta {
1960 is_directory: file
1961 .get("is_directory")
1962 .and_then(|d| d.as_bool())
1963 .unwrap_or(false),
1964 size: file.get("size").and_then(|s| s.as_u64()).unwrap_or(0),
1965 time: file
1966 .get("modified_time")
1967 .and_then(|t| t.as_str())
1968 .and_then(parse_time)
1969 .unwrap_or(0),
1970 file_index: next_index(),
1971 };
1972 entries.push_back((name.to_string(), meta));
1973 }
1974 debug!(
1975 "SMB2 QUERY_DIRECTORY: {} entries for {}",
1976 entries.len(),
1977 path
1978 );
1979 if let Some(h) = s.files.get_mut(&file_id) {
1980 h.listing = Some(entries);
1981 }
1982 }
1983
1984 let mut s = ctx.state.lock().await;
1986 let Some(pending) = s.files.get_mut(&file_id).and_then(|h| h.listing.as_mut()) else {
1987 return Ok(Some(wire::error_response(
1988 &hdr.with_status(status::FILE_CLOSED),
1989 )));
1990 };
1991 if pending.is_empty() {
1992 return Ok(Some(wire::error_response(
1993 &hdr.with_status(status::NO_MORE_FILES),
1994 )));
1995 }
1996 let mut out: Vec<Vec<u8>> = Vec::new();
1997 let mut used = 0usize;
1998 while let Some((name, meta)) = pending.front() {
1999 let entry = wire::directory_entry(class, name, meta);
2000 let cost = used + entry.len();
2002 if cost > output_len {
2003 break;
2004 }
2005 used += wire::padded_len(&entry);
2006 out.push(entry);
2007 pending.pop_front();
2008 if flags & RETURN_SINGLE_ENTRY != 0 {
2009 break;
2010 }
2011 }
2012 if out.is_empty() {
2013 return Ok(Some(wire::error_response(
2014 &hdr.with_status(STATUS_INFO_LENGTH_MISMATCH),
2015 )));
2016 }
2017 Ok(Some(wire::query_directory_response(
2018 &hdr,
2019 &wire::join_directory_entries(&out),
2020 )))
2021 }
2022
2023 fn transact_too_large(req: &RequestHeader, output_len: u32, ctx: &Ctx<'_>) -> Vec<u8> {
2026 Log::new(Some(ctx.status_tx)).warn(format!(
2027 "SMB2 command 0x{:04x} asks for a {} byte output buffer \
2028 (decision=fail_closed_transact_too_large, MaxTransactSize {}); replying \
2029 STATUS_INVALID_PARAMETER",
2030 req.command, output_len, MAX_TRANSACT_SIZE
2031 ));
2032 wire::error_response(&ResponseHeader::for_request(req, status::INVALID_PARAMETER))
2033 }
2034
2035 async fn consult_llm(
2037 ctx: &Ctx<'_>,
2038 operation: &str,
2039 params: serde_json::Value,
2040 ) -> Result<Vec<serde_json::Value>> {
2041 Log::new(Some(ctx.status_tx)).debug(format!(
2042 "Consulting LLM for SMB {} operation: {:?}",
2043 operation, params
2044 ));
2045
2046 let mut event_data = serde_json::json!({
2047 "operation": operation,
2048 });
2049 if let Some(obj) = params.as_object() {
2050 for (key, value) in obj {
2051 if key == "path" && value.as_str().is_some_and(str::is_empty) {
2052 continue;
2053 }
2054 if key != "operation" {
2055 event_data[key] = value.clone();
2056 }
2057 }
2058 }
2059
2060 let event = Event::new(&SMB_OPERATION_EVENT, event_data);
2061
2062 Log::new(Some(ctx.status_tx)).trace(format!("Calling LLM for SMB {} operation", operation));
2063
2064 let execution_result = call_llm(
2065 ctx.llm_client,
2066 ctx.app_state,
2067 ctx.server_id,
2068 None, &event,
2070 ctx.protocol.as_ref(),
2071 )
2072 .await?;
2073
2074 for message in &execution_result.messages {
2075 Log::new(Some(ctx.status_tx)).info(message.to_string());
2076 }
2077
2078 debug!(
2079 "LLM returned {} actions for SMB {}",
2080 execution_result.raw_actions.len(),
2081 operation
2082 );
2083
2084 Ok(execution_result.raw_actions)
2085 }
2086
2087 fn llm_failure_response(
2098 req: &RequestHeader,
2099 operation: &str,
2100 path: &str,
2101 err: &anyhow::Error,
2102 ctx: &Ctx<'_>,
2103 ) -> Vec<u8> {
2104 let overloaded = crate::llm::is_overload_error(err);
2105 let code = status_for_llm_failure(err);
2106
2107 Log::new(Some(ctx.status_tx)).warn(format!(
2108 "SMB {} {}: LLM {} (decision=fail_closed_llm_error) - refusing with NTSTATUS \
2109 0x{:08X}: {}",
2110 operation,
2111 path,
2112 if overloaded {
2113 "overloaded"
2114 } else {
2115 "backend failure"
2116 },
2117 code,
2118 err
2119 ));
2120
2121 wire::error_response(&ResponseHeader::for_request(req, code))
2122 }
2123}
2124
2125fn disposition_name(disposition: u32) -> &'static str {
2126 match disposition {
2127 0 => "supersede",
2128 1 => "open",
2129 2 => "create",
2130 3 => "open_if",
2131 4 => "overwrite",
2132 5 => "overwrite_if",
2133 _ => "unknown",
2134 }
2135}
2136
2137fn file_id_for(index: u64) -> [u8; 16] {
2139 let mut id = [0u8; 16];
2140 id[..8].copy_from_slice(&index.to_le_bytes());
2141 id[8..].copy_from_slice(&(index ^ 0x4E47_5342_0000_0000).to_le_bytes());
2142 id
2143}
2144
2145pub fn status_for_llm_failure(err: &anyhow::Error) -> u32 {
2150 if crate::llm::is_overload_error(err) {
2151 status::INSUFFICIENT_RESOURCES
2152 } else {
2153 status::INTERNAL_ERROR
2154 }
2155}
2156
2157fn wildcard_match(pattern: &str, name: &str) -> bool {
2159 let p: Vec<char> = pattern.to_lowercase().chars().collect();
2160 let n: Vec<char> = name.to_lowercase().chars().collect();
2161 let (mut pi, mut ni) = (0usize, 0usize);
2162 let (mut star, mut mark) = (None::<usize>, 0usize);
2163 while ni < n.len() {
2164 if pi < p.len() && (p[pi] == '?' || p[pi] == n[ni]) {
2165 pi += 1;
2166 ni += 1;
2167 } else if pi < p.len() && p[pi] == '*' {
2168 star = Some(pi);
2169 mark = ni;
2170 pi += 1;
2171 } else if let Some(s) = star {
2172 pi = s + 1;
2173 mark += 1;
2174 ni = mark;
2175 } else {
2176 return false;
2177 }
2178 }
2179 while pi < p.len() && p[pi] == '*' {
2180 pi += 1;
2181 }
2182 pi == p.len()
2183}
2184
2185fn parse_time(s: &str) -> Option<u64> {
2187 let t = chrono::DateTime::parse_from_rfc3339(s).ok()?;
2188 Some(wire::filetime_from_unix(
2189 t.timestamp(),
2190 t.timestamp_subsec_nanos(),
2191 ))
2192}
2193
2194fn filetime_now() -> u64 {
2195 let now = crate::utils::clock::SystemTime::now()
2196 .duration_since(crate::utils::clock::UNIX_EPOCH)
2197 .unwrap_or_default();
2198 wire::filetime_from_unix(now.as_secs() as i64, now.subsec_nanos())
2199}
2200
2201fn server_challenge() -> [u8; 8] {
2204 let t = filetime_now();
2205 (t ^ t.rotate_left(29) ^ 0x9E37_79B9_7F4A_7C15).to_le_bytes()
2206}
2207
2208struct SmbReader<R> {
2212 inner: R,
2213 pending_bytes: u64,
2215 pending_reads: u64,
2217}
2218
2219impl<R: tokio::io::AsyncRead + Unpin> SmbReader<R> {
2220 fn new(inner: R) -> Self {
2221 Self {
2222 inner,
2223 pending_bytes: 0,
2224 pending_reads: 0,
2225 }
2226 }
2227
2228 async fn read_exact_counted(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2231 let result = self.inner.read_exact(buf).await;
2232 if let Ok(n) = result {
2233 self.pending_bytes += n as u64;
2234 self.pending_reads += 1;
2235 }
2236 result
2237 }
2238
2239 fn take_pending(&mut self) -> (u64, u64) {
2241 (
2242 std::mem::take(&mut self.pending_bytes),
2243 std::mem::take(&mut self.pending_reads),
2244 )
2245 }
2246}
2247
2248async fn flush_read_stats<R: tokio::io::AsyncRead + Unpin>(
2250 reader: &mut SmbReader<R>,
2251 app_state: &AppState,
2252 server_id: ServerId,
2253 connection_id: ConnectionId,
2254) {
2255 let (bytes, reads) = reader.take_pending();
2256 if bytes == 0 && reads == 0 {
2257 return;
2258 }
2259 app_state
2260 .update_connection_stats(
2261 server_id,
2262 connection_id,
2263 Some(bytes),
2264 None,
2265 Some(reads),
2266 None,
2267 )
2268 .await;
2269}
2270
2271async fn write_counted<W>(
2275 write_half: &Arc<Mutex<W>>,
2276 data: &[u8],
2277 ctx: &Ctx<'_>,
2278) -> std::io::Result<()>
2279where
2280 W: tokio::io::AsyncWrite + Unpin,
2281{
2282 {
2283 let mut writer = write_half.lock().await;
2284 writer.write_all(data).await?;
2285 writer.flush().await?;
2286 }
2287 ctx.app_state
2288 .update_connection_stats(
2289 ctx.server_id,
2290 ctx.connection_id,
2291 None,
2292 Some(data.len() as u64),
2293 None,
2294 Some(1),
2295 )
2296 .await;
2297 Ok(())
2298}
2299
2300async fn read_body_exact<R: tokio::io::AsyncRead + Unpin>(
2303 stream: &mut SmbReader<R>,
2304 buf: &mut [u8],
2305 deadline: std::time::Duration,
2306) -> std::io::Result<()> {
2307 match tokio::time::timeout(deadline, stream.read_exact_counted(buf)).await {
2308 Ok(read) => read.map(|_| ()),
2309 Err(_) => Err(std::io::Error::new(
2310 std::io::ErrorKind::TimedOut,
2311 "peer stalled part-way through an announced SMB2 message",
2312 )),
2313 }
2314}