Skip to main content

rustsmb/
main.rs

1//! rustsmb binary entry point: CLI parsing, async accept loop, observability.
2
3mod 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
19/// Default TCP listen port when `--port` is not supplied.
20const DEFAULT_PORT: u16 = 4450;
21/// Interval between durable-handle expiry sweeps.
22const DURABLE_SWEEP_INTERVAL_SECS: u64 = 30;
23/// Bytes drained from a stray HTTP probe before replying.
24const HTTP_PROBE_READ_LEN: usize = 1024;
25
26/// Command-line interface.
27#[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    /// TCP port to listen on (direct hosting on 445 by tradition).
36    #[arg(short = 'p', long = "port", default_value_t = DEFAULT_PORT)]
37    port: u16,
38
39    /// Publish a share: NAME=PATH (repeatable; defaults to public=$PWD).
40    #[arg(short = 's', long = "share", value_name = "NAME=PATH")]
41    shares: Vec<String>,
42
43    /// Add an account USER:PASSWORD (repeatable; empty DB maps all users to guest).
44    #[arg(short = 'u', long = "user", value_name = "USER:PASSWORD")]
45    users: Vec<String>,
46
47    /// Log filter (tracing EnvFilter syntax); RUST_LOG overrides this value.
48    #[arg(long = "log", default_value = "info")]
49    log_filter: String,
50
51    /// Serve Prometheus metrics at http://ADDR/metrics (e.g. 127.0.0.1:9449).
52    #[arg(long = "metrics-bind", value_name = "ADDR")]
53    metrics_bind: Option<SocketAddr>,
54
55    /// Reject clients that do not sign their traffic.
56    #[arg(long = "require-signing")]
57    require_signing: bool,
58
59    /// Seal authenticated sessions that offer an encryption cipher
60    /// (SMB2_SESSION_FLAG_ENCRYPT_DATA).
61    #[arg(long = "encrypt")]
62    encrypt: bool,
63
64    /// Mark a share as requiring SMB3 encryption by NAME (repeatable). Its
65    /// TREE_CONNECT response sets SMB2_SHAREFLAG_ENCRYPT_DATA and all traffic
66    /// on the tree is sealed.
67    #[arg(long = "encrypt-share", value_name = "NAME")]
68    encrypt_shares: Vec<String>,
69
70    /// Advertise SMB2_SHAREFLAG_COMPRESS_DATA on a share by NAME (repeatable):
71    /// its TREE_CONNECT response signals the client may compress traffic.
72    #[arg(long = "compress-share", value_name = "NAME")]
73    compress_shares: Vec<String>,
74
75    /// Mark a share as continuously available by NAME (repeatable): its
76    /// TREE_CONNECT response advertises SMB2_SHAREFLAG_CONTINUOUSLY_AVAILABLE
77    /// and it may grant persistent handles ([MS-SMB2] §3.3.5.9.11).
78    #[arg(long = "ca-share", value_name = "NAME")]
79    ca_shares: Vec<String>,
80
81    /// Durable-handle store backend: `mem` (non-durable) or `redb` (survives
82    /// a server restart, enabling persistent-handle reclaim).
83    #[arg(long = "handle-store", default_value = "mem")]
84    handle_store: String,
85
86    /// Path for the `redb` handle-store database.
87    #[arg(long = "handle-store-path", default_value = "rustsmb-handles.redb")]
88    handle_store_path: String,
89}
90
91/// Entry point.
92fn main() {
93    let args = Args::parse();
94
95    // Observability: tracing subscriber honours RUST_LOG, else --log filter.
96    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    // Single-threaded io_uring runtime: all networking and file I/O run on
130    // one thread via io_uring, so per-connection state stays `!Send` and never
131    // crosses threads. (Per-core scaling with SO_REUSEPORT comes later.)
132    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        // Periodically evict expired durable handles awaiting reconnect.
156        {
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
189/// Build the published share table plus the virtual IPC$ share.
190fn 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    // Virtual IPC$ share for named-pipe traffic.
222    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
237/// Parse USER:PASSWORD account specs into the user database.
238fn 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
250/// Build the durable-handle store from `--handle-store`. `redb` persists across
251/// a restart (persistent-handle reclaim); anything else is the in-memory store.
252fn 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
268/// Serve /metrics over a minimal HTTP/1.0 responder — no framework needed
269/// for a single static endpoint, and scrape latency stays negligible.
270fn 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                        // Drain the request line/headers before responding.
288                        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}