Go BSD-2-Clause

go-smb2

SMB2/3 client library written in Go.

H

hirochachacha

Dernière activité 29 sept. 2026
hirochachacha/go-smb2

410

étoiles

117

forks

19

issues ouvertes

gogolangsmbsmb2

Ce README est souvent en anglais.

smb2

Build Status Go Reference

Description

SMB2/3 client implementation for Go.

Features

  • 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/fs interface support and context.Context support.

Installation

Requires Go 1.27 or later.

go get github.com/hirochachacha/go-smb2/v2

Documentation

http://godoc.org/github.com/hirochachacha/go-smb2/v2

Examples

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.

File manipulation

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))
}

List share names

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)
 }
}

Resolve the authenticated user

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.

Glob and WalkDir through FS interface

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)
 }
}

Check error types

_, 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)) // true

Transparent DFS access

import (
    "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.

Low-level requests

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()

NTLM domain selection

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.

Custom transport settings

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,
  },
 },
}

Kerberos authentication

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.

Integration Testing

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.

// client_conf.json — place in the repository root for integration tests.
// Remove comments before saving: the loader accepts standard JSON only.
[
  {
    "name": "samba-ntlm",
    "benchmark": true, // Use share1 for Go versus OS-native benchmarks.
    "max_credit_balance": 128,
    "transport": {
      "type": "tcp",
      "host": "localhost",
      "port": 445
    },
    "conn": {
      "signing": true // Require message signing.
      // ,"dialect": 785 // Optional: 528 = SMB 2.1, 770 = 3.0.2, 785 = 3.1.1.
                        // Omit to negotiate automatically.
      // ,"guid": "" // Currently unused by the test loader.
    },
    "session": {
      "type": "ntlm",
      "user": "USERNAME",
      "passwd": "PASSWORD",
      "domain": "WORKGROUP"
      // ,"workstation": "CLIENT"
      // ,"targetSPN": "cifs/server.example.com"
    },
    "tree_conn": {
      "share1": "writable",
      "share2": "readonly"
    }
  },
  {
    "name": "samba-kerberos",
    "transport": {
      "type": "tcp",
      "host": "server.example.com",
      "port": 445
    },
    "conn": {"signing": true},
    "session": {
      "type": "kerberos",
      "user": "USERNAME",
      "passwd": "PASSWORD",
      "realm": "EXAMPLE.COM",
      "krb5Config": "/etc/krb5.conf"
      // ,"targetSPN": "cifs/server.example.com"
    },
    "tree_conn": {
      "share1": "writable",
      "share2": "readonly"
    }
    // Uncomment to select the dedicated SMB 2.1 / 3.0.2 / 3.1.1 test
    // instead of ordinary file tests. share2 is then unused; share1 must
    // allow unencrypted access, and encrypted_share must require encryption.
    // ,"kerberos": {"encrypted_share": "encrypted"}
  },
  {
    "name": "samba-quic",
    "transport": {
      "type": "quic",
      "host": "server.example.com",
      "port": 443
      // Optional TLS settings; omitted values use the host and system roots.
      // ,"tls": {
      //   "server_name": "server.example.com",
      //   "ca_file": "/path/to/ca.pem" // PEM; relative paths use the working directory.
      // }
    },
    "conn": {"signing": true, "dialect": 785},
    "session": {
      "type": "ntlm", // Kerberos session settings also apply to QUIC.
      "user": "USERNAME",
      "passwd": "PASSWORD",
      "domain": "WORKGROUP"
    },
    "tree_conn": {"share1": "writable", "share2": "readonly"}
  },
  {
    "name": "samba-dfs", // Dedicated DFS test; excluded from ordinary file tests.
    "transport": {"type": "tcp", "host": "127.0.0.1", "port": 445},
    "session": {
      "type": "ntlm",
      "user": "USERNAME",
      "passwd": "PASSWORD",
      "domain": "WORKGROUP"
    },
    "tree_conn": {"share1": "dfs"}, // Namespace share; share2 is unused.
    "dfs": {
      // Distinct logical server names, all using the transport endpoint above.
      "target": "127.0.0.2",
      "second_target": "127.0.0.3",
      "link": "link"
      // Required namespace links:
      // link, link-alias -> 127.0.0.2 / dfs-target
      // link-extra       -> 127.0.0.3 / dfs-encrypted / nested
      // dfs-encrypted must require SMB encryption.
    }
  }
]

Projets similaires

Python SMBv2 and v3 Client

Python
Jjborean93
401 étoiles95

SMB2/3 userspace client

Ccifsclientfilesystems
Ssahlberg
430 étoiles207

Free, Open Source, User-Mode SMB 1.0/CIFS, SMB 2.0, SMB 2.1 and SMB 3.0 server and client library

C#
TTalAloni
885 étoiles227