SMB Server Phase 2: VFS backend build fix + integration test
- Add VfsFile: Send supertrait for Mutex compatibility - Fix SmbServerCommand: struct → Subcommand enum with Start variant - Fix tracing_subscriber::init() → try_init() to avoid panic when logger already initialized - Fix CLI subcommand name: smb-server → smb-start (flatten naming) - Add #[command(name = "smb-start")] for CLI disambiguation - Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs) - Remove unused VfsFile imports (webdav.rs, scp_handler.rs) - Integration test: Docker smbclient verified (list, upload, read)
This commit is contained in:
39
vendor/smb-server/src/conn/mod.rs
vendored
Normal file
39
vendor/smb-server/src/conn/mod.rs
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
//! Per-connection task layout.
|
||||
|
||||
pub mod reader;
|
||||
pub mod state;
|
||||
pub mod writer;
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::server::ServerState;
|
||||
use state::Connection;
|
||||
|
||||
/// Runs the reader and writer tasks for a single accepted connection until
|
||||
/// either side hangs up. Returns once both halves are done.
|
||||
pub async fn connection_loop(stream: TcpStream, server: Arc<ServerState>) -> io::Result<()> {
|
||||
let (read_half, write_half) = tokio::io::split(stream);
|
||||
let conn = Arc::new(Connection::new(
|
||||
server.config.server_guid,
|
||||
server.config.max_read_size,
|
||||
server.config.max_write_size,
|
||||
));
|
||||
let conn_id = server.active_connections.register(&conn).await;
|
||||
let (tx, rx) = mpsc::channel::<writer::FramePayload>(writer::WRITER_CHANNEL);
|
||||
|
||||
let writer_handle = tokio::spawn(writer::writer_task(write_half, rx));
|
||||
|
||||
info!("connection accepted");
|
||||
let reader_result = reader::reader_task(read_half, server.clone(), conn.clone(), tx).await;
|
||||
debug!(?reader_result, "reader exited");
|
||||
// Wait for writer to drain.
|
||||
let _ = writer_handle.await;
|
||||
server.active_connections.unregister(conn_id).await;
|
||||
info!("connection closed");
|
||||
reader_result
|
||||
}
|
||||
80
vendor/smb-server/src/conn/reader.rs
vendored
Normal file
80
vendor/smb-server/src/conn/reader.rs
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
//! Per-connection frame reader: pulls bytes off the socket, frames them,
|
||||
//! hands each frame to the dispatcher.
|
||||
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::proto::framing::{FRAME_HEADER_LEN, decode_frame_header};
|
||||
use tokio::io::{AsyncReadExt, ReadHalf};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::conn::state::Connection;
|
||||
use crate::server::ServerState;
|
||||
|
||||
/// Read one frame's payload (without the 4-byte length prefix).
|
||||
///
|
||||
/// Returns `Ok(None)` on a clean EOF, `Ok(Some(bytes))` on a complete frame,
|
||||
/// `Err` on partial/garbled data.
|
||||
pub async fn read_one_frame(reader: &mut ReadHalf<TcpStream>) -> io::Result<Option<Vec<u8>>> {
|
||||
let mut hdr = [0u8; FRAME_HEADER_LEN];
|
||||
match reader.read_exact(&mut hdr).await {
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let len = match decode_frame_header(&hdr) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string()));
|
||||
}
|
||||
};
|
||||
let mut payload = vec![0u8; len as usize];
|
||||
reader.read_exact(&mut payload).await?;
|
||||
Ok(Some(payload))
|
||||
}
|
||||
|
||||
/// Continuously read frames; for each, await `dispatch_one`'s response and
|
||||
/// route it to the writer.
|
||||
///
|
||||
/// Sequential dispatch keeps v1 simple and matches the spec's "single writer
|
||||
/// task / per-frame dispatch" pattern. We process one frame at a time per
|
||||
/// connection in v1 — a follow-up can spawn dispatch tasks if a workload
|
||||
/// proves to need credit-window concurrency.
|
||||
pub async fn reader_task(
|
||||
mut reader: ReadHalf<TcpStream>,
|
||||
server: Arc<ServerState>,
|
||||
conn: Arc<Connection>,
|
||||
tx: tokio::sync::mpsc::Sender<crate::conn::writer::FramePayload>,
|
||||
) -> io::Result<()> {
|
||||
loop {
|
||||
let frame = match read_one_frame(&mut reader).await {
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
debug!("client closed connection");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "frame read error");
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
// Check shutdown after every frame.
|
||||
if server
|
||||
.shutting_down
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
debug!("server shutting down; dropping connection");
|
||||
return Ok(());
|
||||
}
|
||||
// The dispatcher is async but we await it inline — order-preserving and
|
||||
// good enough for v1.
|
||||
let response = crate::dispatch::dispatch_frame(&server, &conn, &frame).await;
|
||||
if let Some(bytes) = response
|
||||
&& tx.send(bytes).await.is_err()
|
||||
{
|
||||
debug!("writer channel closed; reader exiting");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
328
vendor/smb-server/src/conn/state.rs
vendored
Normal file
328
vendor/smb-server/src/conn/state.rs
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
//! Connection / session / tree / open state held during a single TCP
|
||||
//! connection's lifetime.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::proto::auth::ntlm::{Identity, NtlmServer};
|
||||
use crate::proto::crypto::{PreauthIntegrity, SigningAlgo};
|
||||
use crate::proto::messages::{Dialect, FileId};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::backend::Handle;
|
||||
use crate::builder::Access;
|
||||
use crate::path::SmbPath;
|
||||
use crate::server::ShareBindings;
|
||||
|
||||
/// In-flight NTLM acceptor + a `is_raw_ntlmssp` flag (true = raw, false =
|
||||
/// SPNEGO-wrapped). The handler hands the second-round response back in the
|
||||
/// same form the client opened with.
|
||||
pub type PendingAuth = Arc<Mutex<(NtlmServer, bool)>>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One connection's negotiated state and its session/tree/open tables.
|
||||
pub struct Connection {
|
||||
pub server_guid: Uuid,
|
||||
pub client_guid: tokio::sync::RwLock<Uuid>,
|
||||
pub dialect: tokio::sync::RwLock<Option<Dialect>>,
|
||||
pub signing_algo: tokio::sync::RwLock<SigningAlgo>,
|
||||
/// Connection.PreauthIntegrityHashValue after NEGOTIATE. SMB 3.1.1
|
||||
/// SESSION_SETUP exchanges fork this into `session_preauth`.
|
||||
pub preauth: Mutex<PreauthIntegrity>,
|
||||
/// Granted at NEGOTIATE: large MTU support flag etc.
|
||||
pub max_read_size: tokio::sync::RwLock<u32>,
|
||||
pub max_write_size: tokio::sync::RwLock<u32>,
|
||||
|
||||
/// Sessions keyed by SessionId.
|
||||
pub sessions: RwLock<HashMap<u64, Arc<RwLock<Session>>>>,
|
||||
|
||||
/// In-flight NTLM acceptors keyed by SessionId. We keep them out of
|
||||
/// `Session` because a session is created only after a successful first
|
||||
/// SESSION_SETUP round — between rounds the entry lives here. The
|
||||
/// `bool` records whether the client sent raw NTLMSSP (true) or
|
||||
/// SPNEGO-wrapped (false) so the second-round response matches form.
|
||||
pub pending_auths: RwLock<HashMap<u64, PendingAuth>>,
|
||||
|
||||
/// In-flight SMB 3.1.1 preauth state keyed by SessionId during
|
||||
/// multi-leg SESSION_SETUP.
|
||||
pub session_preauth: RwLock<HashMap<u64, PreauthIntegrity>>,
|
||||
|
||||
/// Monotonic SessionId allocator.
|
||||
next_session_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new(server_guid: Uuid, max_read_size: u32, max_write_size: u32) -> Self {
|
||||
Self {
|
||||
server_guid,
|
||||
client_guid: tokio::sync::RwLock::new(Uuid::nil()),
|
||||
dialect: tokio::sync::RwLock::new(None),
|
||||
signing_algo: tokio::sync::RwLock::new(SigningAlgo::HmacSha256),
|
||||
preauth: Mutex::new(PreauthIntegrity::new()),
|
||||
max_read_size: tokio::sync::RwLock::new(max_read_size),
|
||||
max_write_size: tokio::sync::RwLock::new(max_write_size),
|
||||
sessions: RwLock::new(HashMap::new()),
|
||||
pending_auths: RwLock::new(HashMap::new()),
|
||||
session_preauth: RwLock::new(HashMap::new()),
|
||||
next_session_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_session_id(&self) -> u64 {
|
||||
self.next_session_id.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub async fn close_session(&self, session_id: u64) -> bool {
|
||||
let removed = {
|
||||
let mut sessions = self.sessions.write().await;
|
||||
sessions.remove(&session_id)
|
||||
};
|
||||
if let Some(sess_arc) = removed {
|
||||
close_session_state(&sess_arc).await;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn close_tree(&self, session_id: u64, tree_id: u32) -> bool {
|
||||
let sess_arc = {
|
||||
let sessions = self.sessions.read().await;
|
||||
sessions.get(&session_id).cloned()
|
||||
};
|
||||
let Some(sess_arc) = sess_arc else {
|
||||
return false;
|
||||
};
|
||||
remove_tree_from_session(&sess_arc, tree_id).await
|
||||
}
|
||||
|
||||
pub async fn close_sessions_for_user(&self, user: &str) -> usize {
|
||||
let to_remove = {
|
||||
let sessions = self.sessions.read().await;
|
||||
let mut ids = Vec::new();
|
||||
for (session_id, sess_arc) in sessions.iter() {
|
||||
let sess = sess_arc.read().await;
|
||||
if matches!(&sess.identity, Identity::User { user: session_user, .. } if session_user == user)
|
||||
{
|
||||
ids.push(*session_id);
|
||||
}
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
let mut removed = 0;
|
||||
for session_id in to_remove {
|
||||
if self.close_session(session_id).await {
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
pub async fn close_trees_for_share(&self, share_name: &str) -> usize {
|
||||
self.close_matching_trees(|_, tree| tree.share.name.eq_ignore_ascii_case(share_name))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn close_trees_for_user_share(&self, user: &str, share_name: &str) -> usize {
|
||||
self.close_matching_trees(|sess, tree| {
|
||||
matches!(&sess.identity, Identity::User { user: session_user, .. } if session_user == user)
|
||||
&& tree.share.name.eq_ignore_ascii_case(share_name)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn close_matching_trees(
|
||||
&self,
|
||||
matches_tree: impl Fn(&Session, &TreeConnect) -> bool,
|
||||
) -> usize {
|
||||
let sessions: Vec<_> = {
|
||||
let sessions = self.sessions.read().await;
|
||||
sessions.values().cloned().collect()
|
||||
};
|
||||
|
||||
let mut removed = 0;
|
||||
for sess_arc in sessions {
|
||||
let tree_ids = {
|
||||
let sess = sess_arc.read().await;
|
||||
let trees = sess.trees.read().await;
|
||||
let mut ids = Vec::new();
|
||||
for (tree_id, tree_arc) in trees.iter() {
|
||||
let tree = tree_arc.read().await;
|
||||
if matches_tree(&sess, &tree) {
|
||||
ids.push(*tree_id);
|
||||
}
|
||||
}
|
||||
ids
|
||||
};
|
||||
|
||||
for tree_id in tree_ids {
|
||||
if remove_tree_from_session(&sess_arc, tree_id).await {
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
}
|
||||
|
||||
async fn close_session_state(sess_arc: &Arc<RwLock<Session>>) {
|
||||
let sess = sess_arc.write().await;
|
||||
let trees: Vec<_> = sess.trees.write().await.drain().collect();
|
||||
for (_tree_id, tree_arc) in trees {
|
||||
close_tree_state(&tree_arc).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_tree_from_session(sess_arc: &Arc<RwLock<Session>>, tree_id: u32) -> bool {
|
||||
let removed = {
|
||||
let sess = sess_arc.read().await;
|
||||
let mut trees = sess.trees.write().await;
|
||||
trees.remove(&tree_id)
|
||||
};
|
||||
if let Some(tree_arc) = removed {
|
||||
close_tree_state(&tree_arc).await;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn close_tree_state(tree_arc: &Arc<RwLock<TreeConnect>>) {
|
||||
let tree = tree_arc.write().await;
|
||||
let opens: Vec<_> = tree.opens.write().await.drain().collect();
|
||||
for (_fid, open_arc) in opens {
|
||||
let mut open = open_arc.write().await;
|
||||
if let Some(handle) = open.handle.take() {
|
||||
let _ = handle.close().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct Session {
|
||||
pub id: u64,
|
||||
pub identity: Identity,
|
||||
pub session_base_key: [u8; 16],
|
||||
pub signing_key: [u8; 16],
|
||||
/// Whether signing is required for this session's traffic.
|
||||
pub signing_required: bool,
|
||||
pub trees: RwLock<HashMap<u32, Arc<RwLock<TreeConnect>>>>,
|
||||
/// 3.1.1: snapshot taken at SESSION_SETUP completion (after the request
|
||||
/// hash but before the response is hashed). Used as KDF context.
|
||||
pub preauth_snapshot: Option<[u8; 64]>,
|
||||
|
||||
next_tree_id: AtomicU32,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(
|
||||
id: u64,
|
||||
identity: Identity,
|
||||
session_base_key: [u8; 16],
|
||||
signing_key: [u8; 16],
|
||||
signing_required: bool,
|
||||
preauth_snapshot: Option<[u8; 64]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
identity,
|
||||
session_base_key,
|
||||
signing_key,
|
||||
signing_required,
|
||||
trees: RwLock::new(HashMap::new()),
|
||||
preauth_snapshot,
|
||||
next_tree_id: AtomicU32::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_tree_id(&self) -> u32 {
|
||||
self.next_tree_id.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn is_anonymous(&self) -> bool {
|
||||
matches!(self.identity, Identity::Anonymous)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TreeConnect
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct TreeConnect {
|
||||
pub id: u32,
|
||||
pub share: Arc<ShareBindings>,
|
||||
pub granted_access: Access,
|
||||
pub opens: RwLock<HashMap<FileId, Arc<RwLock<Open>>>>,
|
||||
next_volatile: AtomicU64,
|
||||
}
|
||||
|
||||
impl TreeConnect {
|
||||
pub fn new(id: u32, share: Arc<ShareBindings>, granted_access: Access) -> Self {
|
||||
Self {
|
||||
id,
|
||||
share,
|
||||
granted_access,
|
||||
opens: RwLock::new(HashMap::new()),
|
||||
next_volatile: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alloc_file_id(&self) -> FileId {
|
||||
let v = self.next_volatile.fetch_add(1, Ordering::Relaxed);
|
||||
FileId::new(v, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Open / DirCursor
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub struct Open {
|
||||
pub file_id: FileId,
|
||||
pub handle: Option<Box<dyn Handle>>,
|
||||
pub granted_access: Access,
|
||||
pub last_path: SmbPath,
|
||||
pub is_directory: bool,
|
||||
pub delete_on_close: bool,
|
||||
pub search_state: Option<DirCursor>,
|
||||
}
|
||||
|
||||
impl Open {
|
||||
pub fn new(
|
||||
file_id: FileId,
|
||||
handle: Box<dyn Handle>,
|
||||
granted_access: Access,
|
||||
last_path: SmbPath,
|
||||
is_directory: bool,
|
||||
delete_on_close: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_id,
|
||||
handle: Some(handle),
|
||||
granted_access,
|
||||
last_path,
|
||||
is_directory,
|
||||
delete_on_close,
|
||||
search_state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator state for a directory listing across multiple QUERY_DIRECTORY
|
||||
/// calls. We snapshot the entries once and consume them in order; subsequent
|
||||
/// calls advance `next` until exhaustion.
|
||||
pub struct DirCursor {
|
||||
pub entries: Vec<crate::backend::DirEntry>,
|
||||
pub next: usize,
|
||||
/// The pattern fixed on the first scan; `RESTART_SCANS` resets `next`.
|
||||
pub pattern: Option<String>,
|
||||
}
|
||||
32
vendor/smb-server/src/conn/writer.rs
vendored
Normal file
32
vendor/smb-server/src/conn/writer.rs
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Per-connection writer task: serializes responses, applies signing, and
|
||||
//! frames the bytes onto the wire.
|
||||
|
||||
use crate::proto::framing::encode_frame;
|
||||
use tokio::io::{AsyncWriteExt, WriteHalf};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error};
|
||||
|
||||
/// One packet of bytes to send. Already includes the final SMB2 header +
|
||||
/// body, *with signing already applied if required*.
|
||||
pub type FramePayload = Vec<u8>;
|
||||
|
||||
/// Writer-task channel size: large enough that a slow remote rarely backs up
|
||||
/// the dispatcher.
|
||||
pub const WRITER_CHANNEL: usize = 64;
|
||||
|
||||
pub async fn writer_task(mut writer: WriteHalf<TcpStream>, mut rx: mpsc::Receiver<FramePayload>) {
|
||||
while let Some(payload) = rx.recv().await {
|
||||
let mut out = Vec::with_capacity(payload.len() + 4);
|
||||
encode_frame(&payload, &mut out);
|
||||
if let Err(e) = writer.write_all(&out).await {
|
||||
error!(error = %e, "writer task: socket write failed");
|
||||
return;
|
||||
}
|
||||
debug!(len = out.len(), "wrote frame");
|
||||
}
|
||||
// Channel closed — flush and bail.
|
||||
if let Err(e) = writer.shutdown().await {
|
||||
debug!(error = %e, "writer shutdown error (best-effort)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user