SMB2/3 client implementation for Go.
- Dialects: SMB 2.0.2, 2.1, 3.0, 3.0.2, and 3.1.1.
- Transports: Direct TCP and SMB over QUIC (requires SMB 3.1.1).
- Authentication: NTLMv2 and Kerberos.
- Encryption: Transparent encryption via AES-128-CCM (SMB 3.0+) and AES-128-GCM / AES-256-CCM / AES-256-GCM (SMB 3.1.1).
- Zero-Copy I/O: Zero-copy reads and writes for unencrypted and uncompressed traffic.
- Symlinks: Symbolic link evaluation and creation via NTFS reparse points.
- DFS: Automatic Distributed File System (DFS) referral resolution.
- Go Integration:
io/fsinterface support andcontext.Contextsupport.
Requires Go 1.27 or later.
go get github.com/hirochachacha/go-smb2/v2
http://godoc.org/github.com/hirochachacha/go-smb2/v2
A smb2.Dialer creates an independent Session for each Dial call. Mount a
share by name; its operations take share-relative paths. Share.Unmount
disconnects that tree, while Session.Close closes the connection and invalidates
all its Shares and Files. The Dialer owns no connections and needs no Close.
Do not modify its configuration while it is in use, including by a DFS client.
For transparent DFS and cross-server symbolic links, use client.New(dialer).
Its path operations take absolute UNCs. It owns and reuses Sessions and Shares,
including sessions used only to retrieve referrals. By default, a Session and
its Shares and connection are closed after 10 seconds with no active operation
or open File using that Session. Pass client.WithSessionIdleTimeout(0) to
client.New to disable idle expiry and keep cached Sessions and Shares until
Client.Close. Close cancels connection establishment and invalidates open
Files. Custom credential and transport factories must cooperate with context
cancellation.
Cancellation does not always return control immediately. Kerberos KDC exchanges use the authentication dependency's timeouts and cannot be interrupted by a context. CREATE, TREE_CONNECT, and LOCK requests may wait for the server's final response so that handles, trees, and locks can be cleaned up safely. A canceled TREE_CONNECT allows five seconds for disconnecting a tree created by a late successful response. This cleanup timeout does not bound the wait for that final response; an unresponsive server can delay cancellation until the connection is closed.
For both smb2.File and client.File, concurrent ReadAt calls are supported.
WriteAt calls may run concurrently with ReadAt or WriteAt on non-overlapping
byte ranges. If a write overlaps another operation, ordering and contents are
unspecified. Callers must serialize all other operations on the same File,
including Read, Write, Seek, directory enumeration, Close, and Truncate,
against other operations on that File. Copies require exclusive use of both
files. Context adapters share the underlying File and these restrictions.
Separate File objects and independent requests sharing a connection can operate
concurrently. These guarantees are narrower than those of Go's os.File.
O_APPEND supports single-writer append: before each non-empty Write, the
library queries EOF, then writes at that offset. This query-and-write sequence
is not atomic and does not provide full POSIX atomic-append guarantees across
independently opened file handles, sessions, or clients. Applications must
coordinate multiple writers to the same file; the library does not implicitly
acquire SMB locks for append operations.
As with os.File, the behavior of Seek on an O_APPEND file is unspecified.
Append opens require ordinary write permission, rather than append-only access.
Copies within a share can use server-side copy when the source and destination
positions match and the destination is not opened with O_APPEND. Copies to
append-opened destinations or between different positions use client-side reads
and writes.
package main
import (
"context"
"fmt"
"io"
"github.com/hirochachacha/go-smb2/v2"
"github.com/hirochachacha/go-smb2/v2/auth"
)
func main() {
dialer := &smb2.Dialer{
Credentials: auth.NTLMCredential{
User: "USERNAME",
Password: "PASSWORD",
},
}
ctx := context.Background()
session, err := dialer.Dial(ctx, "SERVERNAME")
if err != nil {
panic(err)
}
defer session.Close()
fs, err := session.Mount(ctx, "SHARENAME")
if err != nil {
panic(err)
}
defer fs.Unmount(ctx)
f, err := fs.Create(ctx, "hello.txt")
if err != nil {
panic(err)
}
defer fs.Remove(ctx, "hello.txt")
defer f.Close(ctx)
_, err = f.Write(ctx, []byte("Hello world!"))
if err != nil {
panic(err)
}
_, err = f.Seek(ctx, 0, io.SeekStart)
if err != nil {
panic(err)
}
bs, err := io.ReadAll(f.WithContext(ctx))
if err != nil {
panic(err)
}
fmt.Println(string(bs))
}package main
import (
"context"
"fmt"
"github.com/hirochachacha/go-smb2/v2"
"github.com/hirochachacha/go-smb2/v2/auth"
)
func main() {
dialer := &smb2.Dialer{
Credentials: auth.NTLMCredential{
User: "USERNAME",
Password: "PASSWORD",
},
}
session, err := dialer.Dial(context.Background(), "SERVERNAME")
if err != nil {
panic(err)
}
defer session.Close()
names, err := session.ListShareNames(context.Background())
if err != nil {
panic(err)
}
for _, name := range names {
fmt.Println(name)
}
}The user package resolves account names and SIDs through LSARPC on IPC$.
Import github.com/hirochachacha/go-smb2/v2/user and use the session
from the example above:
ctx := context.Background()
ipc, err := session.IPC(ctx)
if err != nil {
panic(err)
}
users, err := user.NewClient(ctx, ipc)
if err != nil {
panic(err)
}
defer users.Close(ctx)
identity, err := users.Current(ctx)
if err != nil {
panic(err)
}
fmt.Printf("%s\\%s: %s\n", identity.Domain, identity.Name, identity.SID)Use Lookup(ctx, name) or LookupSID(ctx, sid) to resolve other accounts.
The session owns IPC$; do not unmount it. Session.Close releases it.
package main
import (
"context"
"fmt"
iofs "io/fs"
"github.com/hirochachacha/go-smb2/v2"
"github.com/hirochachacha/go-smb2/v2/auth"
)
func main() {
dialer := &smb2.Dialer{
Credentials: auth.NTLMCredential{
User: "USERNAME",
Password: "PASSWORD",
},
}
session, err := dialer.Dial(context.Background(), "SERVERNAME")
if err != nil {
panic(err)
}
defer session.Close()
fs, err := session.Mount(context.Background(), "SHARENAME")
if err != nil {
panic(err)
}
defer fs.Unmount(context.Background())
bound := fs.WithContext(context.Background())
matches, err := iofs.Glob(bound, "*")
if err != nil {
panic(err)
}
for _, match := range matches {
fmt.Println(match)
}
err = iofs.WalkDir(bound, ".", func(path string, d iofs.DirEntry, err error) error {
fmt.Println(path, d, err)
return nil
})
if err != nil {
panic(err)
}
}_, err = fs.Open(context.Background(), "notExist.txt")
fmt.Println(errors.Is(err, os.ErrNotExist)) // true
fmt.Println(errors.Is(err, os.ErrExist)) // false
fs.WriteFile(context.Background(), "hello2.txt", []byte("test"), 0444)
err = fs.WriteFile(context.Background(), "hello2.txt", []byte("test2"), 0444)
fmt.Println(errors.Is(err, os.ErrPermission)) // true
ctx, cancel := context.WithTimeout(context.Background(), 0)
defer cancel()
_, err = fs.Open(ctx, "hello.txt")
fmt.Println(errors.Is(err, context.DeadlineExceeded)) // trueimport (
"context"
"github.com/hirochachacha/go-smb2/v2"
"github.com/hirochachacha/go-smb2/v2/client"
)
c := client.New(&smb2.Dialer{Credentials: credentials})
defer c.Close()
data, err := c.ReadFile(ctx, `\\server\share\folder\file.txt`)
if err != nil {
panic(err)
}
fmt.Println(string(data))Open returns a *client.File that wraps *smb2.File bound to the actual target
tree. File.Name and user-facing path errors use the original UNC, and the
embedded file serves I/O. File.WithContext remains available. The client
also supports MkdirAll and RemoveAll. Use io/fs.Glob with
WithContext for glob matching.
Client.WithContext exposes an io/fs filesystem with server/share/path
names. Its virtual root lists currently cached servers, not all servers on
the network. Server directories list shares; uncached servers can also be
accessed directly. For example, using the standard io/fs package:
network := c.WithContext(ctx)
project, err := fs.Sub(network, "server/share/project")
if err != nil {
panic(err)
}
data, err := fs.ReadFile(project, "config.json")Server names in UNCs and referral targets are connection endpoints. Automatic domain classification and domain-controller discovery are not provided. Symlink targets may be relative or absolute UNCs; creating or reading a link does not connect to its target. Rename across resolved shares is not supported.
Manual callers can use errors.As to inspect *protocol.DFSReferralRequiredError,
get the IPC$ share with ipc, err := session.IPC(ctx), then call
dfs.NewClient(ipc).GetReferrals(ctx, referral.Path) and explicitly
connect to a target.
*protocol.CrossShareSymlinkError supplies ResolvedPath, a complete continuation UNC with the
unparsed suffix already applied. GetReferrals also accepts an empty DOMAIN
request or a domain-only DC request and returns name-list information directly.
Pass dfs.WithSiteName("SiteA") to GetReferrals for site-aware referral
ordering.
The low-level API lets you work directly with the SMB2 protocol.
The APIs in x/protocol and x/wire are experimental and have no stability
guarantee; they may change without backward compatibility.
response, err := share.Request().
WithFileID(fileID).
QueryInfo(wire.SMB2_0_INFO_FILE, wire.FileStandardInformation, 0, 24).
Do(ctx)
if err != nil {
return err
}
defer response.Close()
query, err := response.QueryInfo(0)
if err != nil {
return err
}
info, err := query.FileStandardInformation()
if err != nil {
return err
}
size := info.EndOfFile()auth.NTLMCredential.Domain is a *string. Leave it nil to use the
server's challenge TargetName as the authentication domain. Set it to a
string pointer to use that domain exactly, including an empty string:
domain := "" // Use "WORKGROUP", for example, to select an explicit domain.
credentials := auth.NTLMCredential{
User: "USERNAME",
Password: "PASSWORD",
Domain: &domain,
}TargetSPN on auth.NTLMCredential and all three Kerberos configuration types
is a *string: nil generates cifs/<server>, and a non-nil pointer supplies
an explicit SPN. For NTLM, an empty string omits the client-supplied target
name. For Kerberos, an explicit empty SPN is a configuration error.
By default, Dialer.Dial connects to Direct TCP on port 445 using TCPDialer{}.
You can configure a custom port or supply a net.Dialer with custom dial
timeouts, keep-alive periods, or local address bindings:
dialer := &smb2.Dialer{
Credentials: auth.NTLMCredential{
User: "USERNAME",
Password: "PASSWORD",
},
TransportDialer: smb2.TCPDialer{
Port: 8445,
Dialer: &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
},
},
}To connect using SMB over QUIC (UDP port 443 by default), configure
QUICDialer. It uses the smb ALPN and requires SMB 3.1.1. A nil TLS
configuration uses the system trust roots. Supply a CA pool when the server
certificate is not trusted by the system:
dialer := &smb2.Dialer{
Credentials: auth.NTLMCredential{
User: "USERNAME",
Password: "PASSWORD",
},
TransportDialer: smb2.QUICDialer{
TLSConfig: &tls.Config{
RootCAs: roots,
},
},
}auth.NewKerberosCredential accepts one auth.KerberosConfig:
auth.KerberosPassword, auth.KerberosKeytab, or auth.KerberosCCache.
Each configuration holds all settings for that authentication method. The constructor
logs in and returns a *auth.KerberosCredential that implements Credentials
and shares its ticket cache across connections. By default, the registered
cifs/<server FQDN> SPN is derived from the server name passed to Dial:
package main
import (
"context"
"os"
"time"
"github.com/hirochachacha/go-smb2/v2"
"github.com/hirochachacha/go-smb2/v2/auth"
)
func main() {
creds, err := auth.NewKerberosCredential(auth.KerberosPassword{
ConfigFile: "/etc/krb5.conf",
User: "USERNAME",
Realm: "EXAMPLE.COM",
Password: os.Getenv("KRB5_PASSWORD"),
})
if err != nil {
panic(err)
}
defer creds.Close()
dialer := &smb2.Dialer{
Credentials: creds,
RequireMessageSigning: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
session, err := dialer.Dial(ctx, "server.example.com")
if err != nil {
panic(err)
}
defer session.Close()
share, err := session.Mount(ctx, "share")
if err != nil {
panic(err)
}
defer share.Unmount(ctx)
}Pass auth.KerberosKeytab{ConfigFile: "...", User: "...", Realm: "...", File: "..."}
for keytab authentication, or auth.KerberosCCache{ConfigFile: "...", File: "..."}
for an existing credential cache. Configurations may be passed as values or non-nil
pointers. The cache supplies its own identity and its
tickets retain their existing lifetime. An empty Realm uses the configuration's
default realm. The underlying Kerberos client currently rejects empty passwords.
Configurations hold no runtime state and are copied by the constructor. The returned
credential owns the ticket cache and password/keytab renewal, is safe for
concurrent use, and must not be copied. Call its Close after all uses. Each
authentication gets a fresh initiator. KDC exchanges use the underlying client's
timeouts; the Dialer.Dial context cannot interrupt service-ticket acquisition.
For NTLM sessions, omit domain (or set it to null) to use the server's
challenge TargetName. Set "domain": "" to authenticate with an empty
domain, or provide a non-empty string to select an explicit domain.