SMB Server Phase 2: VFS backend build fix + integration test
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

- 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:
Warren
2026-06-20 19:42:29 +08:00
parent 45d050c0b3
commit 7eb528d35f
167 changed files with 59897 additions and 12 deletions

View File

@@ -0,0 +1,19 @@
//! CHANGE_NOTIFY handler — v1 always returns NOT_SUPPORTED.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
_conn: &Arc<Connection>,
_hdr: &Smb2Header,
_body: &[u8],
) -> HandlerResponse {
HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED)
}

107
vendor/smb-server/src/handlers/close.rs vendored Normal file
View File

@@ -0,0 +1,107 @@
//! CLOSE handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{CloseRequest, CloseResponse};
use tracing::debug;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::lookup_session_tree;
use crate::ntstatus;
use crate::server::ServerState;
const FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match CloseRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let removed = {
let tree = tree_arc.write().await;
let mut opens = tree.opens.write().await;
opens.remove(&req.file_id)
};
let open_arc = match removed {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
// Pull state out, close the handle, then optionally unlink.
let mut open = open_arc.write().await;
let handle = open.handle.take();
let path = open.last_path.clone();
let delete_on_close = open.delete_on_close;
let want_attrs = req.flags & FLAG_POSTQUERY_ATTRIB != 0;
drop(open);
// Stat before closing if needed.
let info_before_close = if want_attrs {
if let Some(h) = handle.as_ref() {
h.stat().await.ok()
} else {
None
}
} else {
None
};
if let Some(h) = handle {
let _ = h.close().await;
}
if delete_on_close {
let tree = tree_arc.read().await;
let backend = tree.share.backend.clone();
drop(tree);
if let Err(e) = backend.unlink(&path).await {
debug!(error = %e, "delete-on-close unlink failed");
}
}
let resp = CloseResponse {
structure_size: 60,
flags: req.flags & FLAG_POSTQUERY_ATTRIB,
reserved: 0,
creation_time: info_before_close
.as_ref()
.map(|i| i.creation_time)
.unwrap_or(0),
last_access_time: info_before_close
.as_ref()
.map(|i| i.last_access_time)
.unwrap_or(0),
last_write_time: info_before_close
.as_ref()
.map(|i| i.last_write_time)
.unwrap_or(0),
change_time: info_before_close
.as_ref()
.map(|i| i.change_time)
.unwrap_or(0),
allocation_size: info_before_close
.as_ref()
.map(|i| i.allocation_size)
.unwrap_or(0),
end_of_file: info_before_close
.as_ref()
.map(|i| i.end_of_file)
.unwrap_or(0),
file_attributes: info_before_close
.as_ref()
.map(|i| i.attributes())
.unwrap_or(0),
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("encode");
HandlerResponse::ok(buf)
}

194
vendor/smb-server/src/handlers/create.rs vendored Normal file
View File

@@ -0,0 +1,194 @@
//! CREATE handler — open or create a file/directory and allocate a FileId.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{CreateRequest, CreateResponse};
use tracing::{debug, warn};
use crate::backend::{OpenIntent, OpenOptions};
use crate::builder::Access;
use crate::conn::state::{Connection, Open};
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::lookup_session_tree;
use crate::ntstatus;
use crate::path::SmbPath;
use crate::server::ServerState;
use crate::utils::utf16le_to_units;
// MS-SMB2 §2.2.13 access mask flags
const FILE_READ_DATA: u32 = 0x0000_0001;
const FILE_WRITE_DATA: u32 = 0x0000_0002;
const FILE_APPEND_DATA: u32 = 0x0000_0004;
const FILE_READ_ATTRIBUTES: u32 = 0x0000_0080;
const FILE_WRITE_ATTRIBUTES: u32 = 0x0000_0100;
const DELETE: u32 = 0x0001_0000;
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const GENERIC_ALL: u32 = 0x1000_0000;
const MAX_ALLOWED: u32 = 0x0200_0000;
// CreateOptions
const FILE_DIRECTORY_FILE: u32 = 0x0000_0001;
const FILE_NON_DIRECTORY_FILE: u32 = 0x0000_0040;
const FILE_DELETE_ON_CLOSE: u32 = 0x0000_1000;
// CreateDisposition
const FILE_SUPERSEDE: u32 = 0x0000_0000;
const FILE_OPEN: u32 = 0x0000_0001;
const FILE_CREATE: u32 = 0x0000_0002;
const FILE_OPEN_IF: u32 = 0x0000_0003;
const FILE_OVERWRITE: u32 = 0x0000_0004;
const FILE_OVERWRITE_IF: u32 = 0x0000_0005;
// CreateAction in response (MS-SMB2 §2.2.14)
const FILE_OPENED: u32 = 0x0000_0001;
const FILE_CREATED: u32 = 0x0000_0002;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match CreateRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let tree = tree_arc.read().await;
let granted = tree.granted_access;
let backend = tree.share.backend.clone();
drop(tree);
// Decode path.
let units = match utf16le_to_units(&req.name) {
Some(u) => u,
None => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
};
let path = match SmbPath::from_utf16(&units) {
Ok(p) => p,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
};
// Translate disposition.
let intent = match req.create_disposition {
FILE_SUPERSEDE | FILE_OVERWRITE_IF => OpenIntent::OverwriteOrCreate,
FILE_OPEN => OpenIntent::Open,
FILE_CREATE => OpenIntent::Create,
FILE_OPEN_IF => OpenIntent::OpenOrCreate,
FILE_OVERWRITE => OpenIntent::Truncate,
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
// Translate desired access into read/write hints.
let want_read = req.desired_access
& (FILE_READ_DATA | FILE_READ_ATTRIBUTES | GENERIC_READ | GENERIC_ALL | MAX_ALLOWED)
!= 0;
let want_write = req.desired_access
& (FILE_WRITE_DATA
| FILE_APPEND_DATA
| FILE_WRITE_ATTRIBUTES
| DELETE
| GENERIC_WRITE
| GENERIC_ALL
| MAX_ALLOWED)
!= 0;
// Reject writes on a read-only tree.
if want_write && !granted.allows_write() {
warn!(path = %path, "write open on read-only tree");
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
}
// Disposition that creates: requires write permission.
if !granted.allows_write()
&& matches!(
intent,
OpenIntent::Create
| OpenIntent::OpenOrCreate
| OpenIntent::OverwriteOrCreate
| OpenIntent::Truncate
)
{
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
}
let directory = req.create_options & FILE_DIRECTORY_FILE != 0;
let non_directory = req.create_options & FILE_NON_DIRECTORY_FILE != 0;
if directory && non_directory {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let delete_on_close = req.create_options & FILE_DELETE_ON_CLOSE != 0;
let opts = OpenOptions {
read: want_read || !want_write,
write: want_write,
intent,
directory,
non_directory,
delete_on_close,
};
let handle = match backend.open(&path, opts).await {
Ok(h) => h,
Err(e) => {
debug!(error = %e, path = %path, "backend open failed");
return HandlerResponse::err(e.to_nt_status());
}
};
// Stat for the response.
let info = match handle.stat().await {
Ok(i) => i,
Err(e) => {
let _ = handle.close().await;
return HandlerResponse::err(e.to_nt_status());
}
};
// Allocate FileId, register Open.
let tree = tree_arc.write().await;
let file_id = tree.alloc_file_id();
let open = Open::new(
file_id,
handle,
if want_write { granted } else { Access::Read },
path,
info.is_directory,
delete_on_close,
);
let open_arc = Arc::new(tokio::sync::RwLock::new(open));
tree.opens.write().await.insert(file_id, open_arc);
drop(tree);
let create_action = match intent {
OpenIntent::Create => FILE_CREATED,
OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => FILE_OPENED,
OpenIntent::Open | OpenIntent::Truncate => FILE_OPENED,
};
let resp = CreateResponse {
structure_size: 89,
oplock_level: 0,
flags: 0,
create_action,
creation_time: info.creation_time,
last_access_time: info.last_access_time,
last_write_time: info.last_write_time,
change_time: info.change_time,
allocation_size: info.allocation_size,
end_of_file: info.end_of_file,
file_attributes: info.attributes(),
reserved2: 0,
file_id,
create_contexts_offset: 0,
create_contexts_length: 0,
create_contexts: vec![],
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("encode");
HandlerResponse::ok(buf)
}

21
vendor/smb-server/src/handlers/echo.rs vendored Normal file
View File

@@ -0,0 +1,21 @@
//! ECHO handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::EchoResponse;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
_conn: &Arc<Connection>,
_hdr: &Smb2Header,
_body: &[u8],
) -> HandlerResponse {
let mut buf = Vec::new();
EchoResponse::default().write_to(&mut buf).expect("encode");
HandlerResponse::ok(buf)
}

46
vendor/smb-server/src/handlers/flush.rs vendored Normal file
View File

@@ -0,0 +1,46 @@
//! FLUSH handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{FileId, FlushRequest, FlushResponse};
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::{lookup_open, lookup_session_tree};
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match FlushRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let fid = FileId::new(req.file_id_persistent, req.file_id_volatile);
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let open_arc = match lookup_open(&tree_arc, fid).await {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
let res = {
let open = open_arc.read().await;
match open.handle.as_ref() {
Some(h) => h.flush().await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
}
};
if let Err(e) = res {
return HandlerResponse::err(e.to_nt_status());
}
let mut buf = Vec::new();
FlushResponse::default().write_to(&mut buf).expect("encode");
HandlerResponse::ok(buf)
}

59
vendor/smb-server/src/handlers/ioctl.rs vendored Normal file
View File

@@ -0,0 +1,59 @@
//! IOCTL handler — handles FSCTL_VALIDATE_NEGOTIATE_INFO; everything else
//! returns NOT_SUPPORTED.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{Fsctl, IoctlRequest, IoctlResponse};
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::negotiate::{NEGOTIATE_CAPABILITIES, NEGOTIATE_SECURITY_MODE};
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
server: &Arc<ServerState>,
conn: &Arc<Connection>,
_hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match IoctlRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
match req.fsctl() {
Fsctl::ValidateNegotiateInfo => {
// Build VALIDATE_NEGOTIATE_INFO_RESPONSE per MS-SMB2 §2.2.32.6:
// Capabilities (4) | Guid (16) | SecurityMode (2) | Dialect (2) = 24 bytes.
let dialect = conn.dialect.read().await.map(|d| d.as_u16()).unwrap_or(0);
let mut out = Vec::with_capacity(24);
out.extend_from_slice(&NEGOTIATE_CAPABILITIES.to_le_bytes());
out.extend_from_slice(server.config.server_guid.as_bytes());
out.extend_from_slice(&NEGOTIATE_SECURITY_MODE.to_le_bytes());
out.extend_from_slice(&dialect.to_le_bytes());
let resp = IoctlResponse {
structure_size: 49,
reserved: 0,
ctl_code: req.ctl_code,
file_id: req.file_id,
input_offset: 0,
input_count: 0,
output_offset: 0x70,
output_count: out.len() as u32,
flags: 0,
reserved2: 0,
output: out,
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("IOCTL response encodes");
HandlerResponse::ok(buf)
}
Fsctl::DfsGetReferrals | Fsctl::DfsGetReferralsEx => {
HandlerResponse::err(ntstatus::STATUS_FS_DRIVER_REQUIRED)
}
_ => HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
}
}

21
vendor/smb-server/src/handlers/lock.rs vendored Normal file
View File

@@ -0,0 +1,21 @@
//! LOCK handler — v1 returns success without enforcing locks.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::LockResponse;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
_conn: &Arc<Connection>,
_hdr: &Smb2Header,
_body: &[u8],
) -> HandlerResponse {
let mut buf = Vec::new();
LockResponse::default().write_to(&mut buf).expect("encode");
HandlerResponse::ok(buf)
}

View File

@@ -0,0 +1,28 @@
//! LOGOFF handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::LogoffResponse;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
_body: &[u8],
) -> HandlerResponse {
if hdr.session_id == 0 {
return HandlerResponse::err(ntstatus::STATUS_USER_SESSION_DELETED);
}
conn.close_session(hdr.session_id).await;
let mut buf = Vec::new();
LogoffResponse::default()
.write_to(&mut buf)
.expect("encode");
HandlerResponse::ok(buf)
}

64
vendor/smb-server/src/handlers/mod.rs vendored Normal file
View File

@@ -0,0 +1,64 @@
//! Per-command handlers.
//!
//! Each function here builds a `HandlerResponse` for a specific SMB2 command.
//! Handlers receive the parsed request header and a slice of the body bytes;
//! they return either a successful body or `HandlerResponse::err(ntstatus)`.
use std::sync::Arc;
use crate::proto::header::{Command, Smb2Header};
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::ntstatus;
use crate::server::ServerState;
mod change_notify;
mod close;
mod create;
mod echo;
mod flush;
mod ioctl;
mod lock;
mod logoff;
pub(crate) mod negotiate;
mod oplock_break;
mod query_directory;
mod query_info;
mod read;
mod session_setup;
mod set_info;
pub(crate) mod shared;
mod tree_connect;
mod tree_disconnect;
mod write;
/// Top-level command router.
pub async fn dispatch_command(
server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
match hdr.command {
Command::Negotiate => negotiate::handle(server, conn, hdr, body).await,
Command::SessionSetup => session_setup::handle(server, conn, hdr, body).await,
Command::Logoff => logoff::handle(server, conn, hdr, body).await,
Command::TreeConnect => tree_connect::handle(server, conn, hdr, body).await,
Command::TreeDisconnect => tree_disconnect::handle(server, conn, hdr, body).await,
Command::Create => create::handle(server, conn, hdr, body).await,
Command::Close => close::handle(server, conn, hdr, body).await,
Command::Flush => flush::handle(server, conn, hdr, body).await,
Command::Read => read::handle(server, conn, hdr, body).await,
Command::Write => write::handle(server, conn, hdr, body).await,
Command::Lock => lock::handle(server, conn, hdr, body).await,
Command::Ioctl => ioctl::handle(server, conn, hdr, body).await,
Command::Echo => echo::handle(server, conn, hdr, body).await,
Command::QueryDirectory => query_directory::handle(server, conn, hdr, body).await,
Command::ChangeNotify => change_notify::handle(server, conn, hdr, body).await,
Command::QueryInfo => query_info::handle(server, conn, hdr, body).await,
Command::SetInfo => set_info::handle(server, conn, hdr, body).await,
Command::OplockBreak => oplock_break::handle(server, conn, hdr, body).await,
Command::Cancel => HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
}
}

View File

@@ -0,0 +1,223 @@
//! NEGOTIATE handler.
use std::sync::Arc;
use crate::proto::auth::spnego::encode_init_response;
use crate::proto::crypto::SigningAlgo;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{
Dialect, NegotiateContext, NegotiateRequest, NegotiateResponse, PreauthIntegrityCapabilities,
SigningCapabilities,
};
use tracing::info;
use uuid::Uuid;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::ntstatus;
use crate::server::ServerState;
use crate::utils::{fill_random, now_filetime};
// MS-SMB2 §2.2.4 SecurityMode bits. Keep SIGNING_REQUIRED clear: anonymous
// Linux cifs mounts do not send enough NTLM material for the server to derive
// matching SMB3 signing keys.
pub(crate) const NEGOTIATE_SECURITY_MODE: u16 = 0x0001;
const CAP_DFS: u32 = 0x0000_0001;
const CAP_LEASING: u32 = 0x0000_0002;
const CAP_LARGE_MTU: u32 = 0x0000_0004;
pub(crate) const NEGOTIATE_CAPABILITIES: u32 = CAP_DFS | CAP_LEASING | CAP_LARGE_MTU;
pub async fn handle(
server: &Arc<ServerState>,
conn: &Arc<Connection>,
_hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match NegotiateRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
// Pick the highest dialect we support that the client offered.
const SUPPORTED: &[u16] = &[0x0202, 0x0210, 0x0300, 0x0302, 0x0311];
let mut chosen: Option<u16> = None;
for &d in &req.dialects {
if SUPPORTED.contains(&d) {
chosen = match chosen {
None => Some(d),
Some(prev) if d > prev => Some(d),
Some(prev) => Some(prev),
};
}
}
let chosen = match chosen {
Some(d) => d,
None => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
};
let dialect = match Dialect::from_u16(chosen) {
Some(dialect) => dialect,
None => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
};
*conn.dialect.write().await = Some(dialect);
*conn.client_guid.write().await = Uuid::from_bytes(req.client_guid);
*conn.signing_algo.write().await = match dialect {
Dialect::Smb202 | Dialect::Smb210 => SigningAlgo::HmacSha256,
_ => SigningAlgo::AesCmac,
};
// Build SPNEGO security blob (mech-list-only, advertising NTLMSSP).
let security_blob = encode_init_response();
let security_buffer_offset: u16 = 64 + 64; // SMB2 header + fixed NEG response (64 bytes)
let security_buffer_length: u16 = security_blob.len() as u16;
// For 3.1.1 build negotiate contexts.
let mut contexts_bytes: Vec<u8> = Vec::new();
let mut context_count: u16 = 0;
let mut negotiate_context_offset: u32 = 0;
if dialect == Dialect::Smb311 {
// PREAUTH_INTEGRITY_CAPABILITIES
let mut salt = [0u8; 32];
fill_random(&mut salt);
let preauth_caps = PreauthIntegrityCapabilities {
hash_algorithm_count: 1,
salt_length: 32,
hash_algorithms: vec![PreauthIntegrityCapabilities::HASH_SHA512],
salt: salt.to_vec(),
};
let preauth_data = {
use binrw::BinWrite;
let mut c = std::io::Cursor::new(Vec::new());
BinWrite::write(&preauth_caps, &mut c).expect("preauth negotiate context encodes");
c.into_inner()
};
let preauth_ctx = NegotiateContext {
context_type: NegotiateContext::TYPE_PREAUTH_INTEGRITY,
data_length: preauth_data.len() as u16,
reserved: 0,
data: preauth_data,
};
// SIGNING_CAPABILITIES — advertise AES-CMAC.
let signing_caps = SigningCapabilities {
signing_algorithm_count: 1,
signing_algorithms: vec![SigningCapabilities::ALGORITHM_AES_CMAC],
};
let signing_data = {
use binrw::BinWrite;
let mut c = std::io::Cursor::new(Vec::new());
BinWrite::write(&signing_caps, &mut c).expect("signing negotiate context encodes");
c.into_inner()
};
let signing_ctx = NegotiateContext {
context_type: NegotiateContext::TYPE_SIGNING,
data_length: signing_data.len() as u16,
reserved: 0,
data: signing_data,
};
let ctxs = vec![preauth_ctx, signing_ctx];
if let Err(e) = NegotiateContext::encode_list(&ctxs, &mut contexts_bytes) {
tracing::error!(error = %e, "encode_list failed");
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
context_count = ctxs.len() as u16;
// The contexts go after security buffer, 8-byte aligned.
let post_security = security_buffer_offset as u32 + security_buffer_length as u32;
// Round up to next multiple of 8 from the start of the SMB2 header.
negotiate_context_offset = (post_security + 7) & !7;
}
let max_read_size = *conn.max_read_size.read().await;
let max_write_size = *conn.max_write_size.read().await;
let max_transact_size = max_read_size; // common practice
let resp = NegotiateResponse {
structure_size: 65,
security_mode: NEGOTIATE_SECURITY_MODE,
dialect_revision: chosen,
negotiate_context_count_or_reserved: context_count,
server_guid: *server.config.server_guid.as_bytes(),
capabilities: NEGOTIATE_CAPABILITIES,
max_transact_size,
max_read_size,
max_write_size,
system_time: now_filetime(),
server_start_time: server.server_start_filetime,
security_buffer_offset,
security_buffer_length,
negotiate_context_offset_or_reserved2: negotiate_context_offset,
security_buffer: security_blob,
};
let mut body_out = Vec::new();
if let Err(e) = resp.write_to(&mut body_out) {
tracing::error!(error = %e, "encode NEGOTIATE response");
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
// Append padding to align contexts at `negotiate_context_offset`.
if dialect == Dialect::Smb311 && context_count > 0 {
let cur = 64 + body_out.len() as u32; // header + body so far
if cur < negotiate_context_offset {
let pad = (negotiate_context_offset - cur) as usize;
body_out.extend(std::iter::repeat_n(0u8, pad));
}
body_out.extend_from_slice(&contexts_bytes);
}
info!(?dialect, "NEGOTIATE complete");
let mut hr = HandlerResponse::ok(body_out);
hr.skip_signing = true;
hr
}
/// Build the SMB2 NEGOTIATE response sent in reply to an SMB1 multi-protocol
/// NEGOTIATE_REQUEST that listed an SMB2 dialect (MS-SMB2 §3.3.5.3.1).
///
/// We do NOT commit the connection dialect here — the client will follow up
/// with a real SMB2 NEGOTIATE which goes through [`handle`]. This response
/// only tells the client "yes, I speak SMB2; send me an SMB2 NEGOTIATE next".
pub async fn multi_protocol_response(
server: &Arc<ServerState>,
conn: &Arc<Connection>,
chosen: u16,
) -> HandlerResponse {
let security_blob = encode_init_response();
let security_buffer_offset: u16 = 64 + 64;
let security_buffer_length: u16 = security_blob.len() as u16;
let max_read_size = *conn.max_read_size.read().await;
let max_write_size = *conn.max_write_size.read().await;
let max_transact_size = max_read_size;
let resp = NegotiateResponse {
structure_size: 65,
security_mode: NEGOTIATE_SECURITY_MODE,
dialect_revision: chosen,
negotiate_context_count_or_reserved: 0,
server_guid: *server.config.server_guid.as_bytes(),
capabilities: 0,
max_transact_size,
max_read_size,
max_write_size,
system_time: now_filetime(),
server_start_time: server.server_start_filetime,
security_buffer_offset,
security_buffer_length,
negotiate_context_offset_or_reserved2: 0,
security_buffer: security_blob,
};
let mut body_out = Vec::new();
if let Err(e) = resp.write_to(&mut body_out) {
tracing::error!(error = %e, "encode multi-protocol NEGOTIATE response");
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
info!(
chosen = %format_args!("0x{chosen:04X}"),
"SMB1 multi-protocol -> SMB2"
);
let mut hr = HandlerResponse::ok(body_out);
hr.skip_signing = true;
hr
}

View File

@@ -0,0 +1,27 @@
//! OPLOCK_BREAK handler — acknowledge breaks without granting oplocks.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::FileId;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
_conn: &Arc<Connection>,
_hdr: &Smb2Header,
_body: &[u8],
) -> HandlerResponse {
// Echo back the same shape as the notification — structure_size=24, level=0.
let mut buf = Vec::new();
buf.extend_from_slice(&24u16.to_le_bytes()); // structure_size
buf.push(0); // OplockLevel
buf.push(0); // Reserved
buf.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
buf.extend_from_slice(&FileId::any().persistent.to_le_bytes());
buf.extend_from_slice(&FileId::any().volatile.to_le_bytes());
HandlerResponse::ok(buf)
}

View File

@@ -0,0 +1,136 @@
//! QUERY_DIRECTORY handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{FileInfoClass, QueryDirectoryRequest, QueryDirectoryResponse};
use crate::conn::state::{Connection, DirCursor};
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::{lookup_open, lookup_session_tree};
use crate::info_class::{align8, encode_dir_entry};
use crate::ntstatus;
use crate::server::ServerState;
use crate::utils::utf16le_to_string;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match QueryDirectoryRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
if FileInfoClass::from_u8(req.file_information_class).is_none() {
return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS);
}
let class_byte = req.file_information_class;
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
let pattern_str = utf16le_to_string(&req.file_name);
let pattern: Option<String> = if pattern_str.is_empty() || pattern_str == "*" {
None
} else {
Some(pattern_str)
};
let restart = req.flags & QueryDirectoryRequest::FLAG_RESTART_SCANS != 0
|| req.flags & QueryDirectoryRequest::FLAG_REOPEN != 0;
let single_entry = req.flags & QueryDirectoryRequest::FLAG_RETURN_SINGLE_ENTRY != 0;
// Populate or refresh the cursor.
{
let mut open = open_arc.write().await;
if !open.is_directory {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
if open.search_state.is_none() || restart {
let entries = match open.handle.as_ref() {
Some(h) => h.list_dir(pattern.as_deref()).await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
let entries = match entries {
Ok(e) => e,
Err(e) => return HandlerResponse::err(e.to_nt_status()),
};
open.search_state = Some(DirCursor {
entries,
next: 0,
pattern: pattern.clone(),
});
}
}
// Encode entries into the output buffer.
let mut buf: Vec<u8> = Vec::new();
let mut last_offset_pos: Option<usize> = None;
let cap = req.output_buffer_length as usize;
{
let mut open = open_arc.write().await;
let cursor = open.search_state.as_mut().expect("populated above");
loop {
if cursor.next >= cursor.entries.len() {
break;
}
let entry = &cursor.entries[cursor.next];
let file_index = entry.info.file_index;
let mut bytes = encode_dir_entry(class_byte, entry, file_index);
if bytes.is_empty() {
cursor.next += 1;
continue;
}
// Determine total size with padding for chaining.
let entry_aligned = align8(bytes.len());
// If this is *not* the first entry, we already padded the previous
// entry up to entry_aligned. We commit only if total fits.
let prev_len = buf.len();
let total_after = prev_len + entry_aligned;
if total_after > cap && !buf.is_empty() {
// No room for this entry; stop.
break;
}
// Patch previous NextEntryOffset.
if let Some(prev_off) = last_offset_pos {
let delta = (prev_len - prev_off) as u32;
buf[prev_off..prev_off + 4].copy_from_slice(&delta.to_le_bytes());
}
// Track NextEntryOffset position for the entry we are appending.
last_offset_pos = Some(prev_len);
// Append the entry, then pad to 8.
let target_len = prev_len + entry_aligned;
buf.append(&mut bytes);
while buf.len() < target_len {
buf.push(0);
}
cursor.next += 1;
if single_entry {
break;
}
}
}
if buf.is_empty() {
return HandlerResponse::err(ntstatus::STATUS_NO_MORE_FILES);
}
let resp = QueryDirectoryResponse {
structure_size: 9,
output_buffer_offset: 64 + 8,
output_buffer_length: buf.len() as u32,
buffer: buf,
};
let mut out = Vec::new();
resp.write_to(&mut out).expect("encode");
HandlerResponse::ok(out)
}

View File

@@ -0,0 +1,144 @@
//! QUERY_INFO handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{InfoType, QueryInfoRequest, QueryInfoResponse};
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::{lookup_open, lookup_session_tree};
use crate::info_class as ic;
use crate::ntstatus;
use crate::server::ServerState;
const FILE_DEVICE_DISK: u32 = 0x0000_0007;
const FILE_REMOTE_DEVICE: u32 = 0x0000_0010;
// FS attribute flags (MS-FSCC §2.5.1)
const FILE_CASE_SENSITIVE_SEARCH: u32 = 0x0000_0001;
const FILE_CASE_PRESERVED_NAMES: u32 = 0x0000_0002;
const FILE_UNICODE_ON_DISK: u32 = 0x0000_0004;
const FILE_PERSISTENT_ACLS: u32 = 0x0000_0008;
const FILE_FILE_COMPRESSION: u32 = 0x0000_0010;
const FILE_SUPPORTS_HARD_LINKS: u32 = 0x0040_0000;
const FILE_SUPPORTS_EXTENDED_ATTRIBUTES: u32 = 0x0080_0000;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match QueryInfoRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let info_type = match req.info_type_enum() {
Some(t) => t,
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
};
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
// Pull the file index (we use FileId.volatile as the unique handle id).
let (file_index, info_res) = {
let open = open_arc.read().await;
let fid = open.file_id;
match open.handle.as_ref() {
Some(h) => (fid.volatile, h.stat().await),
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
}
};
let buf: Vec<u8> = match info_type {
InfoType::File => {
let info = match info_res {
Ok(i) => i,
Err(e) => return HandlerResponse::err(e.to_nt_status()),
};
match req.file_information_class {
ic::FILE_BASIC_INFORMATION => ic::encode_file_basic_information(&info),
ic::FILE_STANDARD_INFORMATION => ic::encode_file_standard_information(&info),
ic::FILE_INTERNAL_INFORMATION => ic::encode_file_internal_information(file_index),
ic::FILE_EA_INFORMATION => ic::encode_file_ea_information(),
ic::FILE_FULL_EA_INFORMATION => {
return HandlerResponse::err(ntstatus::STATUS_NO_EAS_ON_FILE);
}
ic::FILE_ACCESS_INFORMATION => ic::encode_file_access_information(0x001F_01FF),
ic::FILE_POSITION_INFORMATION => ic::encode_file_position_information(),
ic::FILE_MODE_INFORMATION => ic::encode_file_mode_information(0),
ic::FILE_ALIGNMENT_INFORMATION => ic::encode_file_alignment_information(),
ic::FILE_NAME_INFORMATION => ic::encode_file_name_information(&info.name),
ic::FILE_ALL_INFORMATION => {
ic::encode_file_all_information(&info, file_index, 0x001F_01FF)
}
ic::FILE_NETWORK_OPEN_INFORMATION => {
ic::encode_file_network_open_information(&info)
}
ic::FILE_STREAM_INFORMATION => ic::encode_file_stream_information(&info),
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
}
}
InfoType::FileSystem => {
// For FS info we use the open's tree's backend for context.
let creation_time = info_res.as_ref().map(|i| i.creation_time).unwrap_or(0);
match req.file_information_class {
ic::FS_VOLUME_INFORMATION => {
ic::encode_fs_volume_information(creation_time, 0xCAFE_BABE, "smb-server")
}
ic::FS_SIZE_INFORMATION => {
// 1 PiB free pseudo-volume, 4 KiB cluster.
ic::encode_fs_size_information(
1u64 << 40, // total
1u64 << 39, // free
1, // sectors per cluster
4096, // bytes per sector
)
}
ic::FS_DEVICE_INFORMATION => {
ic::encode_fs_device_information(FILE_DEVICE_DISK, FILE_REMOTE_DEVICE)
}
ic::FS_ATTRIBUTE_INFORMATION => ic::encode_fs_attribute_information(
FILE_CASE_SENSITIVE_SEARCH
| FILE_CASE_PRESERVED_NAMES
| FILE_UNICODE_ON_DISK
| FILE_PERSISTENT_ACLS
| FILE_FILE_COMPRESSION
| FILE_SUPPORTS_HARD_LINKS
| FILE_SUPPORTS_EXTENDED_ATTRIBUTES,
255,
"NTFS",
),
ic::FS_FULL_SIZE_INFORMATION => {
ic::encode_fs_full_size_information(1u64 << 40, 1u64 << 39, 1u64 << 39, 1, 4096)
}
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
}
}
InfoType::Security => ic::encode_minimal_security_descriptor(),
InfoType::Quota => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
};
if buf.len() as u32 > req.output_buffer_length {
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
}
let resp = QueryInfoResponse {
structure_size: 9,
output_buffer_offset: 64 + 8,
output_buffer_length: buf.len() as u32,
buffer: buf,
};
let mut out = Vec::new();
resp.write_to(&mut out)
.expect("QUERY_INFO response encodes");
HandlerResponse::ok(out)
}

62
vendor/smb-server/src/handlers/read.rs vendored Normal file
View File

@@ -0,0 +1,62 @@
//! READ handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{ReadRequest, ReadResponse};
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::{lookup_open, lookup_session_tree};
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match ReadRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let max_read = *conn.max_read_size.read().await;
if req.length > max_read {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
let result = {
let open = open_arc.read().await;
match open.handle.as_ref() {
Some(h) => h.read(req.offset, req.length).await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
}
};
let bytes = match result {
Ok(b) => b,
Err(e) => return HandlerResponse::err(e.to_nt_status()),
};
if bytes.is_empty() && req.length > 0 {
return HandlerResponse::err(ntstatus::STATUS_END_OF_FILE);
}
let resp = ReadResponse {
structure_size: 17,
data_offset: ReadResponse::STANDARD_DATA_OFFSET,
reserved: 0,
data_length: bytes.len() as u32,
data_remaining: 0,
flags: 0,
data: bytes.to_vec(),
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("encode");
HandlerResponse::ok(buf)
}

View File

@@ -0,0 +1,262 @@
//! SESSION_SETUP handler — drives the SPNEGO + NTLMv2 state machine.
use std::sync::Arc;
use crate::proto::auth::ntlm::{Identity, NtlmServer, NtlmTargetInfo, UserCreds};
use crate::proto::auth::spnego::{
NegState, OID_NTLMSSP, decode_init_token, decode_resp_token, encode_resp_token,
};
use crate::proto::crypto::signing_key_30;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{Dialect, SessionSetupRequest, SessionSetupResponse};
use tracing::{debug, info, warn};
use crate::conn::state::{Connection, Session};
use crate::dispatch::HandlerResponse;
use crate::ntstatus;
use crate::server::ServerState;
use crate::utils::{fill_random, now_filetime};
pub async fn handle(
server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match SessionSetupRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let blob = req.security_buffer;
if blob.is_empty() {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
if tracing::enabled!(tracing::Level::DEBUG) {
let mut first8 = String::with_capacity(16);
for b in blob.iter().take(8) {
use std::fmt::Write as _;
write!(&mut first8, "{b:02x}").expect("writing to String cannot fail");
}
tracing::debug!(
first8 = %first8,
len = blob.len(),
sid = hdr.session_id,
"session setup blob"
);
}
// Decide which form the security blob takes:
// * GSS-API NegTokenInit — starts with 0x60.
// * SPNEGO NegTokenResp — starts with 0xa1 ([1] context tag).
// * Raw NTLMSSP message — starts with "NTLMSSP\0" (RFC 4178
// §4.2.1 lets the client skip SPNEGO once the mech is settled; both
// Win11 reauth and Linux cifs.ko use this form).
const NTLMSSP_MAGIC: &[u8] = b"NTLMSSP\0";
let inner_token: Vec<u8>;
let is_first_round: bool;
let is_raw_ntlmssp: bool;
if blob.starts_with(NTLMSSP_MAGIC) {
// Raw NTLMSSP. Decide round by message-type at offset 8.
let msg_type = if blob.len() >= 12 {
u32::from_le_bytes([blob[8], blob[9], blob[10], blob[11]])
} else {
0
};
// 1 = NEGOTIATE (first), 3 = AUTHENTICATE (second). 2 is server-only.
is_first_round = msg_type == 1;
is_raw_ntlmssp = true;
inner_token = blob.to_vec();
} else if blob[0] == 0x60 {
// GSS-API outer wrapper — NegTokenInit.
let init = match decode_init_token(&blob) {
Ok(t) => t,
Err(e) => {
warn!(error = %e, "SPNEGO init decode failed");
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
}
};
if !init.mech_types.iter().any(|m| m == OID_NTLMSSP) {
return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED);
}
inner_token = init.mech_token.unwrap_or_default();
is_first_round = true;
is_raw_ntlmssp = false;
} else {
// NegTokenResp follow-up.
let resp = match decode_resp_token(&blob) {
Ok(r) => r,
Err(e) => {
warn!(error = %e, "SPNEGO resp decode failed");
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
}
};
inner_token = resp.response_token.unwrap_or_default();
is_first_round = false;
is_raw_ntlmssp = false;
}
if is_first_round {
// Allocate a fresh session id and start the NTLM state machine.
let new_sid = conn.alloc_session_id();
let mut server_challenge = [0u8; 8];
fill_random(&mut server_challenge);
let netbios = server.config.netbios_name.clone();
let mut acceptor = NtlmServer::new(
server_challenge,
NtlmTargetInfo::new(netbios.clone(), netbios.clone(), netbios, "", ""),
now_filetime(),
);
// Step 1: parse client NEGOTIATE.
if let Err(e) = acceptor.step1_negotiate(&inner_token) {
warn!(error = %e, "NTLM step1 failed");
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
}
let challenge_blob = acceptor.challenge();
// Reply form mirrors the request: raw NTLMSSP if the client skipped
// SPNEGO, else SPNEGO-wrapped.
let outbound = if is_raw_ntlmssp {
challenge_blob
} else {
encode_resp_token(
NegState::AcceptIncomplete,
Some(OID_NTLMSSP),
Some(&challenge_blob),
None,
)
};
// Stash the acceptor for the next round; remember the form so the
// success response can match.
{
let mut pa = conn.pending_auths.write().await;
pa.insert(
new_sid,
Arc::new(std::sync::Mutex::new((acceptor, is_raw_ntlmssp))),
);
}
let body_out =
build_session_setup_response(ntstatus::STATUS_MORE_PROCESSING_REQUIRED, &outbound, 0);
return HandlerResponse {
body: body_out,
status: ntstatus::STATUS_MORE_PROCESSING_REQUIRED,
override_tree_id: None,
override_session_id: Some(new_sid),
skip_signing: true, // no key yet
take_preauth_snapshot_for_session: None,
};
}
// Follow-up round: look up pending acceptor by session id from header.
let sid = hdr.session_id;
if sid == 0 {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let acceptor_arc = {
let mut pa = conn.pending_auths.write().await;
pa.remove(&sid)
};
let acceptor_arc = match acceptor_arc {
Some(a) => a,
None => return HandlerResponse::err(ntstatus::STATUS_USER_SESSION_DELETED),
};
let users = server.users.table.read().await.clone();
let (outcome, raw_form) = {
let pair = acceptor_arc
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let (acceptor, raw_form) = (&pair.0, pair.1);
let lookup = |u: &str, _d: &str| -> Option<UserCreds> { users.get(u).cloned() };
let outcome = match acceptor.authenticate(&inner_token, lookup) {
Ok(o) => o,
Err(e) => {
info!(error = %e, "NTLM authenticate failed");
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
}
};
(outcome, raw_form)
};
// Anonymous gating.
if matches!(outcome.identity, Identity::Anonymous) && !server.anonymous_allowed().await {
return HandlerResponse::err(ntstatus::STATUS_LOGON_FAILURE);
}
let session_base_key = outcome.session_key;
let dialect = *conn.dialect.read().await;
let signing_key = match dialect {
Some(Dialect::Smb311) => [0u8; 16],
Some(_) => signing_key_30(&session_base_key),
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let session_flags = if matches!(outcome.identity, Identity::Anonymous) {
SessionSetupResponse::FLAG_IS_GUEST
} else {
0
};
let signing_required = false;
let session = Session::new(
sid,
outcome.identity.clone(),
session_base_key,
signing_key,
signing_required,
None,
);
let session_arc = Arc::new(tokio::sync::RwLock::new(session));
{
let mut sessions = conn.sessions.write().await;
sessions.insert(sid, session_arc);
}
// Empty buffer for raw NTLMSSP path; SPNEGO accept-completed for SPNEGO.
let success_buf: Vec<u8> = if raw_form {
Vec::new()
} else {
empty_completed()
};
let body_out =
build_session_setup_response(ntstatus::STATUS_SUCCESS, &success_buf, session_flags);
let take_snapshot = if dialect == Some(Dialect::Smb311) {
Some(sid)
} else {
None
};
info!(?outcome.identity, "session established");
HandlerResponse {
body: body_out,
status: ntstatus::STATUS_SUCCESS,
override_tree_id: None,
override_session_id: Some(sid),
// Anonymous responses are not signed (no key). Signed responses for
// authenticated sessions get signed by the dispatcher's normal path.
skip_signing: matches!(outcome.identity, Identity::Anonymous),
take_preauth_snapshot_for_session: take_snapshot,
}
}
fn build_session_setup_response(_status: u32, spnego_blob: &[u8], session_flags: u16) -> Vec<u8> {
let resp = SessionSetupResponse {
structure_size: 9,
session_flags,
security_buffer_offset: 64 + 8, // SMB2 header + fixed prefix
security_buffer_length: spnego_blob.len() as u16,
security_buffer: spnego_blob.to_vec(),
};
let mut buf = Vec::new();
resp.write_to(&mut buf)
.expect("SESSION_SETUP response encodes");
debug!(len = buf.len(), "SESSION_SETUP response built");
buf
}
fn empty_completed() -> Vec<u8> {
encode_resp_token(NegState::AcceptCompleted, None, None, None)
}

View File

@@ -0,0 +1,143 @@
//! SET_INFO handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{InfoType, SetInfoRequest, SetInfoResponse};
use crate::backend::FileTimes;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::{lookup_open, lookup_session_tree};
use crate::info_class as ic;
use crate::ntstatus;
use crate::path::SmbPath;
use crate::server::ServerState;
use crate::utils::utf16le_to_units;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match SetInfoRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let info_type = match InfoType::from_u8(req.info_type) {
Some(t) => t,
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_INFO_CLASS),
};
if !matches!(info_type, InfoType::File) {
return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED);
}
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
let class = req.file_information_class;
let buffer = req.buffer;
let backend = {
let tree = tree_arc.read().await;
tree.share.backend.clone()
};
let result = match class {
ic::FILE_BASIC_INFORMATION => {
if buffer.len() < 36 {
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
}
let creation = u64::from_le_bytes(buffer[0..8].try_into().unwrap());
let access = u64::from_le_bytes(buffer[8..16].try_into().unwrap());
let write = u64::from_le_bytes(buffer[16..24].try_into().unwrap());
let change = u64::from_le_bytes(buffer[24..32].try_into().unwrap());
// 0 means "do not change", -1 (u64::MAX) means "do not change" too per spec.
let to_some = |v: u64| {
if v == 0 || v == u64::MAX {
None
} else {
Some(v)
}
};
let times = FileTimes {
creation_time: to_some(creation),
last_access_time: to_some(access),
last_write_time: to_some(write),
change_time: to_some(change),
};
let open = open_arc.read().await;
match open.handle.as_ref() {
Some(h) => h.set_times(times).await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
}
}
ic::FILE_END_OF_FILE_INFORMATION => {
if buffer.len() < 8 {
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
}
let new_len = u64::from_le_bytes(buffer[0..8].try_into().unwrap());
let open = open_arc.read().await;
match open.handle.as_ref() {
Some(h) => h.truncate(new_len).await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
}
}
ic::FILE_DISPOSITION_INFORMATION => {
if buffer.is_empty() {
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
}
let mut open = open_arc.write().await;
open.delete_on_close = buffer[0] != 0;
Ok(())
}
ic::FILE_RENAME_INFORMATION => {
// FILE_RENAME_INFORMATION layout (MS-FSCC §2.4.37):
// ReplaceIfExists (1) | Reserved (7) | RootDirectory (8) | FileNameLength (4) | FileName...
if buffer.len() < 20 {
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
}
let name_len = u32::from_le_bytes(buffer[16..20].try_into().unwrap()) as usize;
if buffer.len() < 20 + name_len {
return HandlerResponse::err(ntstatus::STATUS_INFO_LENGTH_MISMATCH);
}
let name_bytes = &buffer[20..20 + name_len];
let units = match utf16le_to_units(name_bytes) {
Some(u) => u,
None => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
};
let new_path = match SmbPath::from_utf16(&units) {
Ok(p) => p,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
};
let from = open_arc.read().await.last_path.clone();
match backend.rename(&from, &new_path).await {
Ok(()) => {
open_arc.write().await.last_path = new_path;
Ok(())
}
Err(e) => Err(e),
}
}
ic::FILE_ALLOCATION_INFORMATION => {
// We don't preallocate; respond OK.
Ok(())
}
_ => return HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
};
if let Err(e) = result {
return HandlerResponse::err(e.to_nt_status());
}
let mut buf = Vec::new();
SetInfoResponse::default()
.write_to(&mut buf)
.expect("encode");
HandlerResponse::ok(buf)
}

View File

@@ -0,0 +1,46 @@
//! Internal helpers shared across handlers — tree/open lookup, etc.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::FileId;
use tokio::sync::RwLock;
use crate::conn::state::{Connection, Open, Session, TreeConnect};
use crate::ntstatus;
/// Look up the session and tree referenced by `hdr`, returning the tree
/// inside the session. Returns the appropriate NTSTATUS on miss.
pub async fn lookup_session_tree(
conn: &Arc<Connection>,
hdr: &Smb2Header,
) -> Result<Arc<RwLock<TreeConnect>>, u32> {
let tid = hdr.tree_id().ok_or(ntstatus::STATUS_INVALID_PARAMETER)?;
let sess_arc = lookup_session(conn, hdr.session_id).await?;
let sess = sess_arc.read().await;
let trees = sess.trees.read().await;
trees
.get(&tid)
.cloned()
.ok_or(ntstatus::STATUS_NETWORK_NAME_DELETED)
}
pub async fn lookup_session(conn: &Arc<Connection>, sid: u64) -> Result<Arc<RwLock<Session>>, u32> {
if sid == 0 {
return Err(ntstatus::STATUS_USER_SESSION_DELETED);
}
let sessions = conn.sessions.read().await;
sessions
.get(&sid)
.cloned()
.ok_or(ntstatus::STATUS_USER_SESSION_DELETED)
}
pub async fn lookup_open(
tree: &Arc<RwLock<TreeConnect>>,
file_id: FileId,
) -> Option<Arc<RwLock<Open>>> {
let tree = tree.read().await;
let opens = tree.opens.read().await;
opens.get(&file_id).cloned()
}

View File

@@ -0,0 +1,140 @@
//! TREE_CONNECT handler — share lookup + authorization.
use std::sync::Arc;
use crate::proto::auth::ntlm::Identity;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{TreeConnectRequest, TreeConnectResponse};
use tracing::{info, warn};
use crate::builder::Access;
use crate::conn::state::{Connection, TreeConnect};
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::lookup_session;
use crate::ntstatus;
use crate::server::{ServerState, ShareMode};
const SHARE_TYPE_DISK: u8 = 0x01;
const SHARE_TYPE_PIPE: u8 = 0x02;
const FILE_GENERIC_READ: u32 = 0x0012_0089;
const FILE_GENERIC_EXECUTE: u32 = 0x0012_00A0;
const FILE_ALL_ACCESS: u32 = 0x001F_01FF;
pub async fn handle(
server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match TreeConnectRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let path = req.path_str().unwrap_or_default();
tracing::debug!(%path, "tree connect path");
let share_name = match extract_share_name(&path) {
Some(s) => s,
None => {
tracing::warn!(%path, "tree connect: empty share name");
return HandlerResponse::err(ntstatus::STATUS_BAD_NETWORK_NAME);
}
};
tracing::debug!(%share_name, "tree connect lookup");
let sess_arc = match lookup_session(conn, hdr.session_id).await {
Ok(s) => s,
Err(s) => return HandlerResponse::err(s),
};
let sess = sess_arc.read().await;
let identity = sess.identity.clone();
drop(sess);
// IPC$: synthetic share. Accept at TREE_CONNECT (Windows always probes
// it before mounting an actual share); downstream CREATE/IOCTL on it
// return NotSupported via the no-op backend.
let share = if share_name.eq_ignore_ascii_case("IPC$") {
crate::server::ShareBindings::ipc()
} else {
match server.find_share(&share_name).await {
Some(s) => s,
None => return HandlerResponse::err(ntstatus::STATUS_BAD_NETWORK_NAME),
}
};
// Authorize.
let acl = share.acl.read().await;
let granted = match authorize(&acl.mode, &acl.users, &identity) {
Some(a) => a,
None => {
warn!(?identity, share = %share.name, "TREE_CONNECT denied");
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
}
};
drop(acl);
// Backend cap.
let granted = if share.backend.capabilities().is_read_only {
granted.clamp_to(Access::Read)
} else {
granted
};
let tree_id = sess_arc.read().await.alloc_tree_id();
let tc = Arc::new(tokio::sync::RwLock::new(TreeConnect::new(
tree_id,
share.clone(),
granted,
)));
{
let sess = sess_arc.read().await;
let mut trees = sess.trees.write().await;
trees.insert(tree_id, tc);
}
let maximal_access = match granted {
Access::Read => FILE_GENERIC_READ | FILE_GENERIC_EXECUTE,
Access::ReadWrite => FILE_ALL_ACCESS,
};
let resp = TreeConnectResponse {
structure_size: 16,
share_type: if share.is_ipc {
SHARE_TYPE_PIPE
} else {
SHARE_TYPE_DISK
},
reserved: 0,
share_flags: 0,
capabilities: 0,
maximal_access,
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("encode");
info!(tree_id, share = %share.name, ?granted, "tree connect");
let mut hr = HandlerResponse::ok(buf);
hr.override_tree_id = Some(tree_id);
hr
}
fn extract_share_name(unc: &str) -> Option<String> {
// \\server\share or \\server\share\
let trimmed = unc.trim_end_matches(['\\', '/']);
let parts: Vec<&str> = trimmed
.split(['\\', '/'])
.filter(|s| !s.is_empty())
.collect();
parts.last().map(|s| s.to_string())
}
fn authorize(
mode: &ShareMode,
users: &std::collections::HashMap<String, Access>,
identity: &Identity,
) -> Option<Access> {
match mode {
ShareMode::Public => Some(Access::ReadWrite),
ShareMode::PublicReadOnly => Some(Access::Read),
ShareMode::AuthenticatedOnly => match identity {
Identity::Anonymous => None,
Identity::User { user, .. } => users.get(user).copied(),
},
}
}

View File

@@ -0,0 +1,36 @@
//! TREE_DISCONNECT handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::TreeDisconnectResponse;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::lookup_session;
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
_body: &[u8],
) -> HandlerResponse {
let tid = match hdr.tree_id() {
Some(t) => t,
None => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
if lookup_session(conn, hdr.session_id).await.is_err() {
return HandlerResponse::err(ntstatus::STATUS_USER_SESSION_DELETED);
}
if !conn.close_tree(hdr.session_id, tid).await {
return HandlerResponse::err(ntstatus::STATUS_NETWORK_NAME_DELETED);
}
let mut buf = Vec::new();
TreeDisconnectResponse::default()
.write_to(&mut buf)
.expect("encode");
HandlerResponse::ok(buf)
}

60
vendor/smb-server/src/handlers/write.rs vendored Normal file
View File

@@ -0,0 +1,60 @@
//! WRITE handler.
use std::sync::Arc;
use crate::proto::header::Smb2Header;
use crate::proto::messages::{WriteRequest, WriteResponse};
use crate::builder::Access;
use crate::conn::state::Connection;
use crate::dispatch::HandlerResponse;
use crate::handlers::shared::{lookup_open, lookup_session_tree};
use crate::ntstatus;
use crate::server::ServerState;
pub async fn handle(
_server: &Arc<ServerState>,
conn: &Arc<Connection>,
hdr: &Smb2Header,
body: &[u8],
) -> HandlerResponse {
let req = match WriteRequest::parse(body) {
Ok(r) => r,
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
};
let max_write = *conn.max_write_size.read().await;
if req.length > max_write {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let tree_arc = match lookup_session_tree(conn, hdr).await {
Ok(t) => t,
Err(s) => return HandlerResponse::err(s),
};
let granted = {
let tree = tree_arc.read().await;
tree.granted_access
};
if !matches!(granted, Access::ReadWrite) {
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
}
let open_arc = match lookup_open(&tree_arc, req.file_id).await {
Some(o) => o,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
};
let result = {
let open = open_arc.read().await;
match open.handle.as_ref() {
Some(h) => h.write_owned(req.offset, req.data).await,
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
}
};
let count = match result {
Ok(n) => n,
Err(e) => return HandlerResponse::err(e.to_nt_status()),
};
let mut buf = Vec::new();
WriteResponse::new(count)
.write_to(&mut buf)
.expect("encode");
HandlerResponse::ok(buf)
}