Skip to content

Commit edcf730

Browse files
committed
Add Docker DFS tests, fix 3 bugs, update docs
Docker: two Samba containers (smb-dfs-root:10456 with msdfs link, smb-dfs-target:10457 with actual files). 4 integration tests. Bugs fixed during Docker testing: - IOCTL InputOffset double-counted Header::SIZE (STATUS_INVALID_PARAMETER) - DFS paths missing server\share prefix (Tree::format_path) - Cross-server routing matched hostname-only instead of addr:port New: ClientConfig.dfs_target_overrides for Docker/non-standard ports. Docs: README moves DFS to "What it does". AGENTS.md, tests/CLAUDE.md, msg/CLAUDE.md, client/CLAUDE.md all updated.
1 parent 87a7d78 commit edcf730

20 files changed

Lines changed: 401 additions & 38 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ src/
5252
cancel.rs # CancelRequest
5353
oplock_break.rs # OplockBreakNotification/Acknowledgment
5454
transform.rs # TransformHeader (encryption), CompressionTransformHeader
55+
dfs.rs # DFS referral request/response wire format
5556
5657
transport/ # Transport abstraction
5758
mod.rs # Transport trait (split send/receive)
@@ -81,6 +82,7 @@ src/
8182
pipeline.rs # Unified operation pipeline
8283
directory.rs # Directory listing helpers
8384
shares.rs # Share enumeration (IPC$ + srvsvc RPC)
85+
dfs.rs # DFS referral IOCTL, DfsResolver with TTL cache
8486
8587
tests/
8688
pack_roundtrip.rs # Property-based tests for pack/unpack
@@ -205,7 +207,7 @@ See `tests/CLAUDE.md` for the full testing guide. Quick reference:
205207

206208
### Docker test containers
207209

208-
12 Samba containers in `tests/docker/internal/`, exercising the full protocol stack:
210+
13 Samba containers in `tests/docker/internal/`, exercising the full protocol stack:
209211

210212
| Container | Port | What it tests |
211213
|-----------------------|-------|-----------------------------------------------|
@@ -220,6 +222,8 @@ See `tests/CLAUDE.md` for the full testing guide. Quick reference:
220222
| smb-50shares | 10453 | 50 shares, RPC enumeration at scale |
221223
| smb-maxreadsize | 10454 | 64 KB max read/write, chunking edge cases |
222224
| smb-encryption-aes128 | 10455 | Mandatory encryption (AES-128-CCM, SMB 3.0.2) |
225+
| smb-dfs-root | 10456 | DFS namespace root with msdfs link |
226+
| smb-dfs-target | 10457 | DFS target server with actual files |
223227

224228
### Tested hardware
225229

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@ slow because it sends one read at a time. Native OS SMB clients pipeline their r
3333
- Streaming downloads and uploads with progress reporting and cancellation
3434
- File watching (CHANGE_NOTIFY for live directory updates)
3535
- Disk space queries (total, free, used)
36+
- DFS path resolution (standalone DFS with transparent referral follow-through)
3637
- Reconnection after network failures
3738
- Auto-flush on writes (data safety for family photos and company docs)
3839

3940
## What it doesn't do (yet)
4041

4142
If you need any of these, check the [`smb`](https://crates.io/crates/smb) crate which supports them:
4243

43-
- **DFS path resolution**: returns `Error::DfsReferralRequired` with the path so you can handle it yourself. Full
44-
automatic DFS follow-through is planned for post-1.0.
4544
- **Multi-channel**: multiple TCP connections to the same server for higher throughput. Planned for post-1.0.
4645
- **QUIC transport**: SMB over QUIC for Azure Files and Windows Server 2022+ over the internet
4746
- **RDMA transport**: datacenter-only, ultra-low-latency storage
@@ -183,7 +182,7 @@ Add to your `Cargo.toml`:
183182

184183
```toml
185184
[dependencies]
186-
smb2 = "0.1"
185+
smb2 = "0.2"
187186
```
188187

189188
You'll also need an async runtime. The library is runtime-agnostic, but [tokio](https://github.com/tokio-rs/tokio) is
@@ -286,7 +285,8 @@ The benchmark tool is included at `benchmarks/smb/`. Run with `cargo run -p smb-
286285
| Limitation | Details |
287286
|-----------------------|-------------------------------------------------------------------|
288287
| No credential cache | Kerberos tickets are fetched fresh each connection (no ccache) |
289-
| No DFS follow-through | Returns `Error::DfsReferralRequired` with the path, you handle it |
288+
| No domain-based DFS | Standalone DFS links work; AD domain-based DFS namespaces are not supported |
289+
| No DFS target failback | Uses the first reachable target; no automatic failback to preferred targets |
290290
| No multi-channel | Single TCP connection per client |
291291
| No QUIC/RDMA | TCP only (covers ~99% of use cases) |
292292
| SMB1 not supported | SMB2/3 only (SMB1 is deprecated and insecure) |
@@ -297,7 +297,7 @@ The benchmark tool is included at `benchmarks/smb/`. Run with `cargo run -p smb-
297297
### vs `smb` crate
298298

299299
The [`smb`](https://crates.io/crates/smb) crate is the most complete Rust SMB2 option right now. It covers more features
300-
than `smb2` (DFS, multi-channel, QUIC, RDMA). If you need those, use it.
300+
than `smb2` (multi-channel, QUIC, RDMA). If you need those, use it.
301301

302302
But for the common case (connect to a NAS, move files around), `smb2` is a better fit:
303303

benchmarks/smb/src/smb2_runner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub async fn connect(target: &Target) -> Result<(SmbClient, Tree), String> {
2626
auto_reconnect: false,
2727
compression: true,
2828
dfs_enabled: true,
29+
dfs_target_overrides: std::collections::HashMap::new(),
2930
};
3031

3132
let mut client = SmbClient::connect(config)

docs/migration-from-smb-crate.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ let mut client = smb2::SmbClient::connect(smb2::ClientConfig {
4848
auto_reconnect: false,
4949
compression: true,
5050
dfs_enabled: true,
51+
dfs_target_overrides: std::collections::HashMap::new(),
5152
}).await?;
5253
```
5354

src/client/CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,6 @@ Tree-level encryption: `connect_share()` checks the share's encrypt flag and act
122122
- **Compound encryption wraps the entire chain**: One TRANSFORM_HEADER for all sub-requests concatenated, not per sub-request.
123123
- **Share-level encryption**: If a share has `SMB2_SHAREFLAG_ENCRYPT_DATA`, encryption is activated even if the session didn't require it.
124124
- **FileDownload/FileUpload can leak handles on drop**: Rust has no async drop. If not consumed fully, the file handle leaks. The types log a warning.
125+
- **DFS paths must include server\share prefix**: When `SMB2_FLAGS_DFS_OPERATIONS` is set, the server expects the path to start with `server\share\` (MS-SMB2 3.2.4.3). `Tree::format_path()` handles this automatically for DFS shares. Without the prefix, Samba strips the first two path components, leading to wrong file opens.
126+
- **DFS redirect changes the tree in-place**: After a DFS redirect, `tree.server`, `tree.share_name`, and `tree.tree_id` all change. Subsequent operations on the same tree use the target server directly -- they must use target-relative paths, not the original DFS paths.
127+
- **tree.server stores addr:port**: The `server` field on `Tree` stores the full `addr:port` string (not just hostname) so `connection_for_tree` can distinguish servers that share the same hostname but use different ports.

src/client/mod.rs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ pub struct ClientConfig {
7878
/// connect to the target server, and retry the operation.
7979
/// Default: true.
8080
pub dfs_enabled: bool,
81+
/// Override addresses for DFS target servers.
82+
///
83+
/// Maps server hostnames (as they appear in DFS referrals) to
84+
/// `host:port` socket addresses. Useful when DFS targets use
85+
/// internal hostnames that the client can't resolve, or when
86+
/// port mapping is needed (for example, Docker test environments).
87+
///
88+
/// Default: empty (use the server hostname from the referral
89+
/// with port 445).
90+
pub dfs_target_overrides: std::collections::HashMap<String, String>,
8191
}
8292

8393
/// A connection to a specific server with its authenticated session.
@@ -137,7 +147,7 @@ impl SmbClient {
137147
conn.compression_enabled()
138148
);
139149

140-
let primary_server = conn.server_name().to_string();
150+
let primary_server = config.addr.clone();
141151

142152
Ok(SmbClient {
143153
config,
@@ -152,7 +162,7 @@ impl SmbClient {
152162
/// Connect using an existing connection and session (for testing).
153163
#[cfg(test)]
154164
pub(crate) fn from_parts(config: ClientConfig, conn: Connection, session: Session) -> Self {
155-
let primary_server = conn.server_name().to_string();
165+
let primary_server = config.addr.clone();
156166
SmbClient {
157167
config,
158168
conn,
@@ -178,7 +188,8 @@ impl SmbClient {
178188
/// and encryption is not already active, encryption is activated
179189
/// using the session's keys.
180190
pub async fn connect_share(&mut self, share_name: &str) -> Result<Tree> {
181-
let tree = Tree::connect(&mut self.conn, share_name).await?;
191+
let mut tree = Tree::connect(&mut self.conn, share_name).await?;
192+
tree.server = self.primary_server.clone();
182193

183194
// Activate encryption if the share requires it and it's not already active.
184195
// Fall back to AES-128-CCM if the server didn't send an encryption
@@ -230,7 +241,7 @@ impl SmbClient {
230241
)
231242
.await?;
232243

233-
self.primary_server = conn.server_name().to_string();
244+
self.primary_server = self.config.addr.clone();
234245
self.conn = conn;
235246
self.session = session;
236247
self.extra_connections.clear();
@@ -305,9 +316,16 @@ impl SmbClient {
305316
tree: &mut Tree,
306317
original_path: &str,
307318
) -> Result<String> {
308-
let server = tree.server.clone();
319+
// Extract hostname (strip port) for UNC path construction.
320+
let hostname = tree
321+
.server
322+
.split(':')
323+
.next()
324+
.unwrap_or(&tree.server)
325+
.to_string();
309326
let share = tree.share_name.clone();
310-
let unc_path = format!("\\\\{}\\{}\\{}", server, share, original_path);
327+
let normalized = original_path.replace('/', "\\");
328+
let unc_path = format!("\\\\{}\\{}\\{}", hostname, share, normalized);
311329

312330
debug!("dfs: resolving {}", unc_path);
313331

@@ -329,7 +347,12 @@ impl SmbClient {
329347
// Try each target (multi-target failover).
330348
let mut last_error = None;
331349
for resolved in &resolved_list {
332-
let target_addr = format!("{}:{}", resolved.server, resolved.port);
350+
let target_addr = self
351+
.config
352+
.dfs_target_overrides
353+
.get(&resolved.server)
354+
.cloned()
355+
.unwrap_or_else(|| format!("{}:{}", resolved.server, resolved.port));
333356

334357
// Get or create connection to target server.
335358
match self.ensure_connection(&target_addr).await {
@@ -402,7 +425,12 @@ impl SmbClient {
402425
.conn
403426
};
404427

405-
Tree::connect(conn, share).await
428+
let mut tree = Tree::connect(conn, share).await?;
429+
// Override server to the full addr:port so connection_for_tree
430+
// can distinguish targets that share the same hostname but
431+
// use different ports (for example, Docker port-mapped containers).
432+
tree.server = target_addr.to_string();
433+
Ok(tree)
406434
}
407435

408436
/// Check whether a DFS retry should be attempted for the given error.
@@ -1023,6 +1051,7 @@ pub async fn connect(addr: &str, username: &str, password: &str) -> Result<SmbCl
10231051
auto_reconnect: false,
10241052
compression: true,
10251053
dfs_enabled: true,
1054+
dfs_target_overrides: std::collections::HashMap::new(),
10261055
})
10271056
.await
10281057
}
@@ -1175,6 +1204,7 @@ mod tests {
11751204
auto_reconnect: false,
11761205
compression: true,
11771206
dfs_enabled: true,
1207+
dfs_target_overrides: std::collections::HashMap::new(),
11781208
};
11791209

11801210
SmbClient::from_parts(config, conn, session)
@@ -1293,6 +1323,7 @@ mod tests {
12931323
auto_reconnect: true,
12941324
compression: true,
12951325
dfs_enabled: true,
1326+
dfs_target_overrides: std::collections::HashMap::new(),
12961327
};
12971328

12981329
let client = SmbClient::from_parts(config, conn, session);

0 commit comments

Comments
 (0)