1mod auth;
4pub mod cmds;
5pub mod dispatch;
6pub mod io;
7pub mod security;
8pub mod session_scope;
9pub mod smb2;
10pub mod srvsvc;
11pub mod state;
12
13use std::collections::HashMap;
14use std::net::SocketAddr;
15use std::sync::Arc;
16
17use clap::Parser as _;
18
19const DEFAULT_PORT: u16 = 4450;
21const DURABLE_SWEEP_INTERVAL_SECS: u64 = 30;
23const HTTP_PROBE_READ_LEN: usize = 1024;
25
26#[derive(Debug, clap::Parser)]
28#[command(
29 name = "rustsmb",
30 version,
31 about = "An SMB1/SMB2/SMB3 file server in pure Rust",
32 long_about = None
33)]
34struct Args {
35 #[arg(short = 'p', long = "port", default_value_t = DEFAULT_PORT)]
37 port: u16,
38
39 #[arg(short = 's', long = "share", value_name = "NAME=PATH")]
41 shares: Vec<String>,
42
43 #[arg(short = 'u', long = "user", value_name = "USER:PASSWORD")]
45 users: Vec<String>,
46
47 #[arg(long = "log", default_value = "info")]
49 log_filter: String,
50
51 #[arg(long = "metrics-bind", value_name = "ADDR")]
53 metrics_bind: Option<SocketAddr>,
54
55 #[arg(long = "require-signing")]
57 require_signing: bool,
58
59 #[arg(long = "encrypt")]
62 encrypt: bool,
63
64 #[arg(long = "encrypt-share", value_name = "NAME")]
68 encrypt_shares: Vec<String>,
69
70 #[arg(long = "compress-share", value_name = "NAME")]
73 compress_shares: Vec<String>,
74
75 #[arg(long = "ca-share", value_name = "NAME")]
79 ca_shares: Vec<String>,
80
81 #[arg(long = "handle-store", default_value = "mem")]
84 handle_store: String,
85
86 #[arg(long = "handle-store-path", default_value = "rustsmb-handles.redb")]
88 handle_store_path: String,
89}
90
91fn main() {
93 let args = Args::parse();
94
95 let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| args.log_filter.clone());
97 tracing_subscriber::fmt()
98 .with_env_filter(tracing_subscriber::EnvFilter::new(filter))
99 .with_target(false)
100 .init();
101
102 if args.shares.is_empty() {
103 let cwd = std::env::current_dir().unwrap_or_default();
104 tracing::warn!(path = %cwd.display(), "no --share given; publishing cwd as 'public'");
105 }
106
107 let share_map = build_shares(&args);
108 let users = build_users(&args);
109
110 let guid = random_guid();
111 let shared = Arc::new(state::ServerShared {
112 shares: share_map,
113 guid,
114 domain: "WORKGROUP".into(),
115 server_name: "RUSTSMB".into(),
116 users,
117 allow_guest: true,
118 require_signing: args.require_signing,
119 encrypt: args.encrypt,
120 locks: Arc::new(state::LockManager::new()),
121 share_modes: Arc::new(state::ShareModeTable::new()),
122 oplocks: Arc::new(state::OplockTable::new()),
123 leases: Arc::new(state::LeaseTable::new()),
124 durables: build_handle_store(&args),
125 sessions: Arc::new(state::SessionTable::new()),
126 app_instances: Arc::new(state::AppInstanceTable::new()),
127 });
128
129 tokio_uring::start(async move {
133 let listener = match tokio_uring::net::TcpListener::bind(SocketAddr::from((
134 [0, 0, 0, 0],
135 args.port,
136 ))) {
137 Ok(l) => l,
138 Err(e) => {
139 tracing::error!(port = args.port, error = %e, "failed to bind");
140 std::process::exit(1);
141 }
142 };
143 tracing::info!(
144 port = args.port,
145 build = "B7",
146 shares = ?shared.shares.keys().collect::<Vec<_>>(),
147 auth = if shared.users.is_empty() { "guest(any)" } else { "users" },
148 "rustsmb listening"
149 );
150
151 if let Some(addr) = args.metrics_bind {
152 spawn_metrics_endpoint(addr);
153 }
154
155 {
157 let durables = shared.durables.clone();
158 tokio_uring::spawn(async move {
159 loop {
160 tokio::time::sleep(std::time::Duration::from_secs(DURABLE_SWEEP_INTERVAL_SECS)).await;
161 let _ = durables.sweep_expired(state::now_ms()).await;
162 }
163 });
164 }
165
166 loop {
167 match listener.accept().await {
168 Ok((stream, peer)) => {
169 let srv = shared.clone();
170 tokio_uring::spawn(async move {
171 let _ = stream.set_nodelay(true);
172 let span = tracing::info_span!("conn", peer = %peer);
173 let _g = span.enter();
174 tracing::debug!("client connected");
175 let transport = Box::new(smb_server_transport::tcp::TcpTransport::new(
176 stream,
177 peer.to_string(),
178 )) as Box<dyn smb_server_transport::Transport>;
179 dispatch::serve_client(srv, transport).await;
180 tracing::debug!("client disconnected");
181 });
182 }
183 Err(e) => tracing::warn!(error = %e, "accept error"),
184 }
185 }
186 });
187}
188
189fn build_shares(args: &Args) -> HashMap<String, state::Share> {
191 let mut shares: Vec<(String, String)> = Vec::new();
192 for spec in &args.shares {
193 match spec.split_once('=') {
194 Some((name, path)) => shares.push((name.to_string(), path.to_string())),
195 None => shares.push(("public".into(), spec.clone())),
196 }
197 }
198 if shares.is_empty() {
199 let cwd = std::env::current_dir().unwrap_or_default();
200 shares.push(("public".into(), cwd.to_string_lossy().into_owned()));
201 }
202
203 let mut map = HashMap::new();
204 let encrypt_set: std::collections::HashSet<String> =
205 args.encrypt_shares.iter().map(|s| s.to_lowercase()).collect();
206 let compress_set: std::collections::HashSet<String> =
207 args.compress_shares.iter().map(|s| s.to_lowercase()).collect();
208 let ca_set: std::collections::HashSet<String> =
209 args.ca_shares.iter().map(|s| s.to_lowercase()).collect();
210 for (name, root) in shares {
211 let lname = name.to_lowercase();
212 let vfs: Arc<dyn smb_server_vfs::Vfs> = Arc::new(smb_server_backend_posix::PosixVfs::new(root.clone()));
213 let encrypt = encrypt_set.contains(&lname);
214 let compress = compress_set.contains(&lname);
215 let ca = ca_set.contains(&lname);
216 map.insert(
217 lname.clone(),
218 state::Share { name, root: root.into(), vfs, is_ipc: false, encrypt, compress, ca },
219 );
220 }
221 map.insert(
223 "ipc$".into(),
224 state::Share {
225 name: "IPC$".into(),
226 root: "/".into(),
227 vfs: Arc::new(smb_server_backend_posix::PosixVfs::new("/")),
228 is_ipc: true,
229 encrypt: false,
230 compress: false,
231 ca: false,
232 },
233 );
234 map
235}
236
237fn build_users(args: &Args) -> HashMap<String, String> {
239 let mut users = HashMap::new();
240 for spec in &args.users {
241 if let Some((user, pass)) = spec.split_once(':') {
242 users.insert(user.to_lowercase(), pass.to_string());
243 } else {
244 tracing::warn!(spec = %spec, "ignoring malformed --user (expected USER:PASSWORD)");
245 }
246 }
247 users
248}
249
250fn build_handle_store(args: &Args) -> Arc<dyn smb_server_handle_store::HandleStore> {
253 match args.handle_store.as_str() {
254 "redb" => match smb_server_handle_store::RedbStore::open(&args.handle_store_path) {
255 Ok(store) => {
256 tracing::info!(path = %args.handle_store_path, "durable handle store: redb");
257 Arc::new(store)
258 }
259 Err(e) => {
260 tracing::error!(error = %e, "failed to open redb handle store; using memory");
261 Arc::new(smb_server_handle_store::MemStore::new())
262 }
263 },
264 _ => Arc::new(smb_server_handle_store::MemStore::new()),
265 }
266}
267
268fn spawn_metrics_endpoint(addr: SocketAddr) {
271 use metrics_exporter_prometheus::PrometheusBuilder;
272 match PrometheusBuilder::new().install_recorder() {
273 Ok(handle) => {
274 tokio_uring::spawn(async move {
275 let listener = match tokio_uring::net::TcpListener::bind(addr) {
276 Ok(l) => l,
277 Err(e) => {
278 tracing::error!(%addr, error = %e, "metrics bind failed");
279 return;
280 }
281 };
282 tracing::info!(%addr, "prometheus metrics on http://{addr}/metrics");
283 loop {
284 let Ok((sock, _)) = listener.accept().await else { continue };
285 let text = handle.render();
286 tokio_uring::spawn(async move {
287 let (_read, _scratch) = sock.read(vec![0u8; HTTP_PROBE_READ_LEN]).await;
289 let resp = format!(
290 "HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\
291 Content-Length: {}\r\nConnection: close\r\n\r\n{}",
292 text.len(),
293 text
294 );
295 let (_wrote, _buf) = sock.write_all(resp.into_bytes()).await;
296 let _ = sock.shutdown(std::net::Shutdown::Write);
297 });
298 }
299 });
300 }
301 Err(e) => tracing::error!(error = %e, "prometheus recorder install failed"),
302 }
303}
304
305fn random_guid() -> [u8; 16] {
306 use std::io::Read;
307 let mut g = [0u8; 16];
308 if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
309 let _ = f.read_exact(&mut g);
310 }
311 g
312}