Files
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

2279 lines
70 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Package nfs4server turns reassembled ONC RPC records into NFSv4.2
// operations against a virtual filesystem: the stateless operations, the
// sessions with their slot table, and the open, lock and delegation state.
package nfs4server
import (
crand "crypto/rand"
"crypto/tls"
"encoding/binary"
"errors"
"io/fs"
"net"
"strconv"
"sync"
"time"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
"sourcedock.dev/petrbalvin/nfs/internal/rpc"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// maxRecord bounds one ONC RPC record. A call larger than this is refused
// before its bytes are buffered.
const maxRecord = 4 << 20
// supportedAccess is the ACCESS mask this server answers for.
const supportedAccess = uint32(nfsfs.AccessRead | nfsfs.AccessLookup |
nfsfs.AccessModify | nfsfs.AccessExtend | nfsfs.AccessDelete | nfsfs.AccessExec)
// The anonymous identity root squash maps a root credential onto: the
// nobody user and group of the classic NFS exports.
const (
nobodyUID = 65534
nobodyGID = 65534
)
// A Handler serves one connection at a time from the FS it is given. It is
// the Handle hook of the server package.
type Handler struct {
FS nfsfs.FS
// LeasePeriod is how long the server keeps state for a client that
// stops renewing. Zero means the default of 90 seconds.
LeasePeriod time.Duration
// StateDir is the directory the client state persists into: handles
// and opens survive a restart when it is set.
StateDir string
// GracePeriod is the reclaim window after the server start. Zero means
// the default of 90 seconds.
GracePeriod time.Duration
// ServerKey is the long term Kerberos key of the nfs service
// principal; set together with ServiceName it enables the RPCSEC_GSS
// procedures of RFC 2203.
ServerKey []byte
ServiceName string
// TLSConfig, when set, lets the client upgrade the connection to
// TLS through the AUTH_TLS probe of RFC 9289.
TLSConfig *tls.Config
// LogOps answers one log line per operation on stderr: the operation,
// the status it returned and the time it took. Off by default.
LogOps bool
// RootSquash maps the root identity of a client onto nobody: a
// credential that claims uid 0 acts as uid 65534 with group 65534,
// so the permission bits of nobody decide and the objects root
// creates carry nobody. Off by default, which keeps the trust AUTH_SYS
// hands to the claim.
RootSquash bool
// DeviceAddr is the universal address the pNFS data server, which is
// the metadata server itself, answers on. Empty means the local
// address of the request connection, with loopback as the last
// resort.
DeviceAddr string
mu sync.Mutex
verifier sync.Once // the boot verifier is generated exactly once
writeVer [8]byte
store *sessionStore
states *stateStore
lockStore *lockStore
delegStore *delegStore
layoutStore *layoutServer
dirDelegSt *dirDelegStore
gssSt *gssStore
refSt *referralStore
nattrSt *nattrStore
grace *grace
probes int
// excl guards and holds the create verifiers of exclusive opens.
exclMu sync.Mutex
excl map[string][8]byte
}
// graced returns the grace window, made once per handler. The verifier
// generation happens outside the handler mutex, because writeVerifier
// takes the same lock.
func (h *Handler) graced() *grace {
h.mu.Lock()
g := h.grace
h.mu.Unlock()
if g != nil {
return g
}
g = newGrace(h.gracePeriod())
h.mu.Lock()
if h.grace == nil {
h.grace = g
}
g = h.grace
h.mu.Unlock()
return g
}
// gracePeriod resolves the configured grace period: an explicitly set
// value wins, zero means the default of 90 seconds.
func (h *Handler) gracePeriod() time.Duration {
if h.GracePeriod != 0 {
return h.GracePeriod
}
return 90 * time.Second
}
// delegs returns the delegation store, made once per handler.
func (h *Handler) delegs() *delegStore {
h.mu.Lock()
defer h.mu.Unlock()
if h.delegStore == nil {
h.delegStore = newDelegStore()
}
return h.delegStore
}
// leasePeriod resolves the configured lease period.
func (h *Handler) leasePeriod() time.Duration {
if h.LeasePeriod > 0 {
return h.LeasePeriod
}
return 90 * time.Second
}
// locks returns the byte range lock store, made once per handler.
func (h *Handler) locks() *lockStore {
h.mu.Lock()
defer h.mu.Unlock()
if h.lockStore == nil {
h.lockStore = newLockStore()
}
return h.lockStore
}
// openStates returns the OPEN state store, made once per handler.
func (h *Handler) openStates() *stateStore {
h.mu.Lock()
defer h.mu.Unlock()
if h.states == nil {
h.states = newStateStore(h.StateDir)
}
return h.states
}
// layouts returns the pNFS layout store, made once per handler.
func (h *Handler) layouts() *layoutServer {
h.mu.Lock()
defer h.mu.Unlock()
if h.layoutStore == nil {
h.layoutStore = newLayoutServer()
}
return h.layoutStore
}
// sessions returns the session store, made once per handler with a
// random server prefix for its session ids.
func (h *Handler) sessions() *sessionStore {
h.mu.Lock()
defer h.mu.Unlock()
if h.store == nil {
var prefix [4]byte
if _, err := crand.Read(prefix[:]); err != nil {
panic("nfs4server: the random source failed: " + err.Error())
}
h.store = newSessionStore(prefix)
}
return h.store
}
// writeVerifier returns the server boot verifier of RFC 8881 section
// 8.10: a value that changes when the server restarts, so a client can
// tell that its write replays are meaningless. It is made once, from the
// system's random source.
func (h *Handler) writeVerifier() [8]byte {
h.verifier.Do(func() {
if _, err := crand.Read(h.writeVer[:]); err != nil {
panic("nfs4server: the random source failed: " + err.Error())
}
})
return h.writeVer
}
// writer returns the mutating half of the filesystem, or nil when the
// backend serves reads only.
func (h *Handler) writer() nfsfs.Writer {
w, _ := h.FS.(nfsfs.Writer)
return w
}
// a cred holds the identity the client asserted, or the identity of nobody
// when the credential flavour carries no usable claim.
type cred struct {
uid, gid uint32
groups []uint32
}
// the fh register of one COMPOUND: the current and the saved handle,
// the client whose session drives the compound, and the component the
// last LOOKUP resolved.
type fhreg struct {
cur, saved nfsfs.Handle
haveCur bool
clientID uint64
lastLookup string
}
// HandleConn serves ONC RPC calls until the connection closes. Errors on
// the wire end the session; protocol errors are answered and it stays.
// An AUTH_TLS NULL probe upgrades the connection to TLS per RFC 9289
// when the handler carries a TLSConfig.
func (h *Handler) HandleConn(conn net.Conn) {
defer conn.Close()
tlsActive := false
writeMu := &sync.Mutex{}
ctx := newConnCB(conn, writeMu)
for {
rec, err := rpc.ReadRecord(conn, maxRecord)
if err != nil {
return
}
_, mtype, err := rpc.PeekHeader(rec)
if err != nil {
return
}
if mtype == rpc.MsgReply {
// A reply to a CB call this connection issued; the CB caller
// waits on its xid.
if !ctx.route(rec) {
return
}
continue
}
call, args, err := rpc.DecodeCall(rec)
if err != nil {
return
}
// The RPC-with-TLS probe: a NULL procedure under AUTH_TLS. The
// answer carries the STARTTLS token and the connection upgrades
// to TLS before any further record is read.
if call.Cred.Flavor == rpc.FlavorTLS {
if call.Procedure != nfs4.ProcNull || tlsActive || h.TLSConfig == nil {
reply := rpc.AppendRejectedReply(nil, call.XID, rpc.AuthBadCred)
if err := ctx.write(reply); err != nil {
return
}
continue
}
reply, err := rpc.AppendAcceptedReply(nil, call.XID,
rpc.Auth{Flavor: rpc.FlavorNone, Body: []byte(rpc.StarttlsToken)},
rpc.AcceptSuccess, rpc.Mismatch{})
if err != nil {
return
}
if err := ctx.write(reply); err != nil {
return
}
tlsConn := tls.Server(conn, h.TLSConfig)
if err := tlsConn.Handshake(); err != nil {
return
}
conn = tlsConn
ctx.conn = tlsConn
tlsActive = true
continue
}
// RFC 9289 section 4.1: a server that carries a TLS certificate
// refuses the procedures of a client that has not upgraded, the
// NULL procedure of a probe or a ping included only as the probe
// itself. The plaintext NULL answers, everything else is auth
// too weak.
if h.TLSConfig != nil && !tlsActive && call.Procedure != nfs4.ProcNull {
reply := rpc.AppendRejectedReply(nil, call.XID, rpc.AuthTooWeak)
if err := ctx.write(reply); err != nil {
return
}
continue
}
c := decodeCred(call.Cred)
if h.RootSquash && c.uid == 0 {
// Root squash: the claim of uid 0 acts as nobody, the
// anonymous identity of the export, wherever the credential
// decides anything afterwards.
c = cred{uid: nobodyUID, gid: nobodyGID, groups: []uint32{nobodyGID}}
}
var reply []byte
switch {
case call.Program != nfs4.Program:
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptProgUnavail, rpc.Mismatch{})
case call.Procedure == nfs4.ProcNull && call.Cred.Flavor == rpc.FlavorGSS &&
len(call.Cred.Body) >= 4 && peekU32(call.Cred.Body) == rpc.GSSVersion3:
// RPCSEC_GSSv3 control procedures ride on NULLPROC: the
// verifier and the protected result are generated together
// under the session lock, in the order a client verifies
// them.
body3, verf3, ok3 := h.gssv3Control(call, args)
if !ok3 {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptGarbageArgs, rpc.Mismatch{})
} else {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, verf3,
rpc.AcceptSuccess, rpc.Mismatch{})
reply = append(reply, body3...)
}
case call.Procedure == nfs4.ProcNull && call.Cred.Flavor == rpc.FlavorGSS &&
len(call.Cred.Body) >= 4 && peekU32(call.Cred.Body) == rpc.GSSVersion1:
// RPCSEC_GSS version one control procedures ride the NULL
// procedure with the context token in the call data, RFC 2203
// section 5.1.3.
cred, derr := rpc.DecodeGSSCred(call.Cred.Body)
switch {
case derr == nil && cred.Proc == rpc.GSSProcInit:
body := h.gssInit(args)
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptSuccess, rpc.Mismatch{})
reply = append(reply, body...)
case derr == nil && cred.Proc == rpc.GSSProcContinue:
// The krb5 profile establishes a context in one token, so
// there is nothing to continue with: the initiator sees
// the major status and starts over.
body := rpc.AppendGSSInitRes(nil, nil, gssMajorContinueNeeded, 0, 0, nil)
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptSuccess, rpc.Mismatch{})
reply = append(reply, body...)
case derr == nil && cred.Proc == rpc.GSSProcDestroy:
if !h.gssDestroy(call) {
reply = rpc.AppendRejectedReply(nil, call.XID, rpc.AuthGSSCredProb)
} else {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptSuccess, rpc.Mismatch{})
}
default:
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptGarbageArgs, rpc.Mismatch{})
}
case call.Procedure == nfs4.ProcNull && call.Cred.Flavor == rpc.FlavorGSS:
// An RPCSEC_GSS credential of a version this server does not
// speak, sent at the NULL procedure: refuse it as garbage.
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptGarbageArgs, rpc.Mismatch{})
case call.Procedure == nfs4.ProcNull:
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptSuccess, rpc.Mismatch{})
case call.Procedure == nfs4.ProcCompound && call.Cred.Flavor == rpc.FlavorGSS:
// A COMPOUND under RPCSEC_GSS: the verifier is checked, the
// arguments unwrapped or verified, the compound runs and the
// results are protected, all under the session lock, so the
// tokens leave the server in the order a client verifies
// them.
body, verf, ok := h.gssCompound(call, args)
if !ok {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptGarbageArgs, rpc.Mismatch{})
} else {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, verf,
rpc.AcceptSuccess, rpc.Mismatch{})
reply = append(reply, body...)
}
case call.Procedure == nfs4.ProcCompound:
body, ok := h.compoundCtx(args, c, ctx)
if !ok {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptGarbageArgs, rpc.Mismatch{})
} else {
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptSuccess, rpc.Mismatch{})
reply = append(reply, body...)
}
default:
reply, err = rpc.AppendAcceptedReply(nil, call.XID, rpc.AuthNull,
rpc.AcceptProcUnavail, rpc.Mismatch{})
}
if err != nil {
return
}
if err := ctx.write(reply); err != nil {
return
}
}
}
// decodeCred maps the credential to the identity the operations evaluate
// against. An AUTH_SYS credential carries the client's claim; anything else
// carries nobody.
func decodeCred(a rpc.Auth) cred {
if a.Flavor == rpc.FlavorSys {
if sys, err := rpc.DecodeAuthSysBody(a.Body); err == nil {
return cred{uid: sys.UID, gid: sys.GID, groups: sys.GIDs}
}
}
return cred{uid: 0xffffffff, gid: 0xffffffff}
}
// isSessionSetupOp names the operations a client may run on a fore
// channel without a session; every other operation requires SEQUENCE
// first, per RFC 8881 section 15.1.3.5.
func isSessionSetupOp(op uint32) bool {
switch op {
case nfs4.OpExchangeID, nfs4.OpCreateSession, nfs4.OpDestroySession,
nfs4.OpDestroyClientID, nfs4.OpBindConnToSession, nfs4.OpBackchannelCtl, nfs4.OpSequence:
return true
}
return false
}
// compound executes one COMPOUND4args and returns the COMPOUND4res. A
// second return of false means the arguments were malformed, which is
// GARBAGE_ARGS at the RPC layer and not an NFS status.
//
// A COMPOUND whose first operation is SEQUENCE runs inside a session: the
// slot table of the session drives at-most-once execution, and a repeated
// sequence replays the cached answer. Every other operation of the
// standard requires the session, except the session setup set.
func (h *Handler) compound(args []byte, c cred) ([]byte, bool) {
return h.compoundCtx(args, c, nil)
}
// compoundCtx is compound with the connection's callback machinery: a
// CREATE_SESSION that negotiates a back channel binds it to this
// connection through ctx.
func (h *Handler) compoundCtx(args []byte, c cred, ctx *connCB) ([]byte, bool) {
header, d, err := nfs4.DecodeCompoundArgs(args)
if err != nil {
return nil, false
}
if header.Minor != nfs4.MinorVersion {
return nfs4.AppendCompoundRes(nil, nfs4.ErrMinorVersMismatch, header.Tag, nil), true
}
var reg fhreg
// The answer accumulates in a single buffer: the header carries
// placeholders for the top level status and the operation count,
// patched in place once the operations have run, so no result is
// copied from an intermediate slice on the way out.
res := make([]byte, 0, 96+8*int(header.OpCount))
res = xdr.AppendUint32(res, 0)
res = xdr.AppendString(res, header.Tag)
opCountOff := len(res)
res = xdr.AppendUint32(res, 0)
nOps := 0
top := uint32(nfs4.ErrOK)
var inSession, replayed, cacheThis bool
var sessID nfs4.SessionID
var slotID uint32
var clientID uint64
for i := range header.OpCount {
op, err := d.Uint32()
if err != nil {
return nil, false
}
if op == nfs4.OpSequence && i != 0 {
top = nfs4.ErrSequencePos
res = nfs4.AppendOpHeader(res, op, top)
nOps++
break
}
if op == nfs4.OpSequence {
a, err := nfs4.DecodeSequenceArgs(d)
if err != nil {
return nil, false
}
// A lease that lapsed before this request frees the client's
// state and ends its identity, RFC 8881 section 8.11: the
// client that comes back after its lease must establish a new
// one, and nothing of the old life may linger to deny others.
if h.sessions().leaseExpired(a.SessionID.ClientIDOf(), h.leasePeriod(), time.Now()) {
h.sessions().destroyClientID(a.SessionID.ClientIDOf())
h.dropClientState(a.SessionID.ClientIDOf())
top = nfs4.ErrExpired
res = nfs4.AppendOpHeader(res, op, top)
nOps++
break
}
sess, replay, status := h.sessions().sequence(a.SessionID, a.Sequence, a.Slot)
if status != nfs4.ErrOK {
top = status
res = nfs4.AppendOpHeader(res, op, top)
nOps++
break
}
inSession = true
sessID, slotID, cacheThis = a.SessionID, a.Slot, a.CacheThis
clientID = a.SessionID.ClientIDOf()
reg.clientID = clientID
h.sessions().renew(clientID, time.Now())
res = nfs4.AppendSequenceRes(nfs4.AppendOpHeader(res, op, nfs4.ErrOK),
sessID, a.Sequence, a.Slot, uint32(len(sess.slots)-1), 0)
nOps++
if replay {
_, cached, ok := h.sessions().replay(sessID, slotID)
if !ok || cached == nil {
return nfs4.AppendCompoundRes(nil, nfs4.ErrRetryUncachedRep, header.Tag, nil), true
}
return cached, true
}
continue
}
if !inSession && !isSessionSetupOp(op) {
top = nfs4.ErrOpNotInSession
res = nfs4.AppendOpHeader(res, op, top)
nOps++
break
}
if h.sessions().leaseExpired(clientID, h.leasePeriod(), time.Now()) {
top = nfs4.ErrExpired
res = nfs4.AppendOpHeader(res, op, top)
nOps++
break
}
start := time.Now()
payload, status, err := h.dispatch(op, d, &reg, c, sessID, clientID, ctx)
if h.LogOps {
logOp(op, status, time.Since(start))
}
if err != nil {
return nil, false
}
res = nfs4.AppendOpHeader(res, op, status)
nOps++
// LOCKT answers the DENIED status with the body of the conflicting
// lock; every other operation carries a body only on success.
if status == nfs4.ErrOK || (op == nfs4.OpLockt && status == nfs4.ErrDenied) {
res = append(res, payload...)
}
if status != nfs4.ErrOK {
top = status
break
}
}
binary.BigEndian.PutUint32(res[0:4], top)
binary.BigEndian.PutUint32(res[opCountOff:opCountOff+4], uint32(nOps))
if inSession && !replayed && cacheThis {
h.sessions().cacheReply(sessID, slotID, top, res)
}
return res, true
}
// dispatch executes one operation and returns its result payload, valid
// only while the status is OK, then the status, then an error that marks
// the arguments as malformed rather than the operation as failed.
// opAccess names the permission each data operation requires on the
// current file handle, and on the saved handle where an operation
// crosses the two: the mask the credential must hold before the
// operation touches the backend. Operations outside the table are state
// bookkeeping or handle juggling, whose stateids carry their own
// validation.
func opAccess(op uint32) (mask, savedMask uint32, enforced bool) {
switch op {
case nfs4.OpRead, nfs4.OpReadPlus, nfs4.OpSeek, nfs4.OpReadlink,
nfs4.OpGetxattr, nfs4.OpListxattr, nfs4.OpReaddir:
return nfsfs.AccessRead, 0, true
case nfs4.OpLookup, nfs4.OpSecinfo, nfs4.OpSecinfoNoName:
return nfsfs.AccessLookup, 0, true
case nfs4.OpWrite, nfs4.OpWriteSame, nfs4.OpSetattr, nfs4.OpCommit,
nfs4.OpAllocate, nfs4.OpDeallocate, nfs4.OpSetxattr, nfs4.OpRemovexattr:
return nfsfs.AccessModify, 0, true
case nfs4.OpCreate, nfs4.OpRemove, nfs4.OpLink:
return nfsfs.AccessModify, 0, true
case nfs4.OpRename:
return nfsfs.AccessModify, nfsfs.AccessModify, true
case nfs4.OpCopy, nfs4.OpClone:
return nfsfs.AccessModify, nfsfs.AccessRead, true
default:
return 0, 0, false
}
}
// authorise checks the credential holds the mask on the current handle.
func (h *Handler) authorise(reg *fhreg, mask uint32, c cred) uint32 {
if !reg.haveCur {
return nfs4.ErrNoFileHandle
}
return h.authoriseHandle(reg.cur, mask, c)
}
// authoriseHandle checks the credential holds the mask on one handle.
func (h *Handler) authoriseHandle(fh nfsfs.Handle, mask uint32, c cred) uint32 {
granted, err := h.FS.Access(fh, mask, c.uid, c.gid, c.groups)
if err != nil {
return mapErr(err)
}
if granted&mask != mask {
return nfs4.ErrAccess
}
return nfs4.ErrOK
}
func (h *Handler) dispatch(op uint32, d *xdr.Decoder, reg *fhreg, c cred, sessID nfs4.SessionID, sessionClientid uint64, ctx *connCB) ([]byte, uint32, error) {
// On a referral stub only the operations that carry the client away
// answer: everything that touches the backend is MOVED, RFC 5661
// section 8.4.2.
if reg.haveCur && h.isReferralStub(reg.cur) &&
op != nfs4.OpGetattr && op != nfs4.OpGetfh && op != nfs4.OpPutfh &&
op != nfs4.OpPutRootfh && op != nfs4.OpSavefh && op != nfs4.OpRestorefh &&
op != nfs4.OpAccess && op != nfs4.OpLookup && op != nfs4.OpSecinfo &&
op != nfs4.OpSecinfoNoName && op != nfs4.OpSequence {
if op == nfs4.OpIllegal {
return nil, nfs4.ErrOpIllegal, nil
}
return nil, nfs4.ErrMoved, nil
}
// Named attribute handles route to their own operations before the
// regular dispatch: the synthetic space has no backend behind it.
if reg.haveCur && h.isNattrFile(reg.cur) {
switch op {
case nfs4.OpGetfh:
return nfs4.AppendGetfhRes(nil, reg.cur), nfs4.ErrOK, nil
case nfs4.OpGetattr:
request, rerr := nfs4.ReadBitmap(d)
if rerr != nil {
return nil, 0, rerr
}
return nfs4.AppendGetattrRes(nil, request, nfs4.Attrs{Type: nfs4.NF4Reg,
FHExpireType: nfs4.FH4Persistent, NamedAttr: true, UniqueHandles: true}), nfs4.ErrOK, nil
case nfs4.OpRead:
if _, rerr := d.Raw(16); rerr != nil {
return nil, 0, rerr
}
off, rerr := d.Uint64()
if rerr != nil {
return nil, 0, rerr
}
count, rerr := d.Uint32()
if rerr != nil {
return nil, 0, rerr
}
value, status := h.nattrRead(reg.cur, off)
if status != nfs4.ErrOK {
return nil, status, nil
}
if uint64(count) < uint64(len(value)) {
value = value[:count]
}
return nfs4.AppendReadRes(nil, true, value), nfs4.ErrOK, nil
case nfs4.OpWrite:
if _, rerr := d.Raw(16); rerr != nil {
return nil, 0, rerr
}
off, rerr := d.Uint64()
if rerr != nil {
return nil, 0, rerr
}
if _, rerr = d.Uint32(); rerr != nil {
return nil, 0, rerr
}
data, rerr := d.VarOpaque()
if rerr != nil {
return nil, 0, rerr
}
count, status := h.nattrWrite(reg.cur, off, data)
if status != nfs4.ErrOK {
return nil, status, nil
}
return nfs4.AppendWriteRes(nil, count, nfs4.StableFileSync, h.writeVerifier()), nfs4.ErrOK, nil
case nfs4.OpSavefh, nfs4.OpRestorefh, nfs4.OpAccess, nfs4.OpCommit:
return nil, nfs4.ErrOK, nil
default:
return nil, nfs4.ErrNotSupp, nil
}
}
if reg.haveCur && h.isNattrDir(reg.cur) {
switch op {
case nfs4.OpGetfh:
return nfs4.AppendGetfhRes(nil, reg.cur), nfs4.ErrOK, nil
case nfs4.OpGetattr:
request, rerr := nfs4.ReadBitmap(d)
if rerr != nil {
return nil, 0, rerr
}
return nfs4.AppendGetattrRes(nil, request, nfs4.Attrs{Type: nfs4.NF4Dir,
FHExpireType: nfs4.FH4Persistent, NamedAttr: true, UniqueHandles: true}), nfs4.ErrOK, nil
case nfs4.OpLookup:
name, rerr := d.String()
if rerr != nil {
return nil, 0, rerr
}
child, status := h.nattrLookup(reg.cur, name)
if status != nfs4.ErrOK {
return nil, status, nil
}
reg.cur, reg.haveCur = child, true
return nil, nfs4.ErrOK, nil
case nfs4.OpCreate:
// The named attribute is made with its initial size from the
// create attributes; the value itself arrives by WRITE. The
// attributes are a full fattr4 of bitmap and list, RFC 8881
// section 18.4.
if _, rerr := d.Uint32(); rerr != nil { // kind, always regular
return nil, 0, rerr
}
name, rerr := d.String()
if rerr != nil {
return nil, 0, rerr
}
request, rerr := nfs4.ReadBitmap(d)
if rerr != nil {
return nil, 0, rerr
}
blob, rerr := d.VarOpaque()
if rerr != nil {
return nil, 0, rerr
}
updates, uerr := nfs4.DecodeSetattrBlob(blob, request)
if uerr != nil {
return nil, nfs4.ErrAttrNotSupp, nil
}
var value []byte
if updates.HasSize {
value = make([]byte, updates.Size)
}
if status2 := h.nattrCreate(reg.cur, name, value); status2 != nfs4.ErrOK {
return nil, status2, nil
}
// The CREATE replaces the current handle with the new named
// attribute file, like every CLAIM_NULL open does.
child, status2 := h.nattrLookup(reg.cur, name)
if status2 != nfs4.ErrOK {
return nil, status2, nil
}
reg.cur, reg.haveCur = child, true
return nfs4.AppendCreateRes(nil), nfs4.ErrOK, nil
case nfs4.OpRemove:
name, rerr := d.String()
if rerr != nil {
return nil, 0, rerr
}
return nfs4.AppendRemoveRes(nil), h.nattrRemove(reg.cur, name), nil
default:
return nil, nfs4.ErrNotSupp, nil
}
}
// The permission gate: a data operation must hold the access its
// mask names on the handle it touches, evaluated against the
// credential the call carried. State bookkeeping, attribute reads
// and handle juggling enforce nothing here; their stateids carry
// their own validation.
if mask, savedMask, enforced := opAccess(op); enforced {
if status := h.authorise(reg, mask, c); status != nfs4.ErrOK {
return nil, status, nil
}
if savedMask != 0 {
if reg.saved == nil {
return nil, nfs4.ErrNoFileHandle, nil
}
if status := h.authoriseHandle(reg.saved, savedMask, c); status != nfs4.ErrOK {
return nil, status, nil
}
}
}
switch op {
case nfs4.OpPutRootfh:
root, err := h.FS.Root()
if err != nil {
return nil, mapErr(err), nil
}
reg.cur, reg.haveCur = root, true
return nil, nfs4.ErrOK, nil
case nfs4.OpPutfh:
fh, err := d.VarOpaque()
if err != nil {
return nil, 0, err
}
reg.cur, reg.haveCur = nfsfs.Handle(fh), true
return nil, nfs4.ErrOK, nil
case nfs4.OpSavefh:
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
reg.saved = reg.cur
return nil, nfs4.ErrOK, nil
case nfs4.OpRestorefh:
if reg.saved == nil {
return nil, nfs4.ErrNoFileHandle, nil
}
reg.cur, reg.haveCur = reg.saved, true
return nil, nfs4.ErrOK, nil
case nfs4.OpGetfh:
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
return nfs4.AppendGetfhRes(nil, reg.cur), nfs4.ErrOK, nil
case nfs4.OpLookup:
name, err := d.String()
if err != nil {
return nil, 0, err
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
// A configured referral lands on a stub handle: the client reads
// the location attributes from it and migrates.
if r, ok := h.referrals().byName(name); ok {
reg.cur, reg.haveCur = h.referrals().put(name, r), true
reg.lastLookup = name
return nil, nfs4.ErrOK, nil
}
child, _, err := h.FS.Lookup(reg.cur, name)
if err != nil {
return nil, mapErr(err), nil
}
reg.cur, reg.haveCur = child, true
reg.lastLookup = name
return nil, nfs4.ErrOK, nil
case nfs4.OpOpenattr:
return h.openattrOp(d, reg)
case nfs4.OpLookupp:
return h.lookuppOp(reg)
case nfs4.OpPutPubfh:
root, err := h.FS.Root()
if err != nil {
return nil, mapErr(err), nil
}
reg.cur, reg.haveCur = root, true
return nil, nfs4.ErrOK, nil
case nfs4.OpVerify:
return h.verifyOp(d, reg, false)
case nfs4.OpNverify:
return h.verifyOp(d, reg, true)
case nfs4.OpGetattr:
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
request, err := nfs4.ReadBitmap(d)
if err != nil {
return nil, 0, err
}
// A referral stub answers the location attributes instead of a
// backend lookup, RFC 5661 section 11.9.1.
if h.isReferralStub(reg.cur) {
return nfs4.AppendGetattrRes(nil, request, h.stubAttrs(reg.cur)), nfs4.ErrOK, nil
}
info, err := h.FS.Getattr(reg.cur)
if err != nil {
return nil, mapErr(err), nil
}
return nfs4.AppendGetattrRes(nil, request, h.attrsOf(reg.cur, info)), nfs4.ErrOK, nil
case nfs4.OpAccess:
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
mask, err := d.Uint32()
if err != nil {
return nil, 0, err
}
granted, err := h.FS.Access(reg.cur, mask, c.uid, c.gid, c.groups)
if err != nil {
return nil, mapErr(err), nil
}
return nfs4.AppendAccessRes(nil, supportedAccess, granted), nfs4.ErrOK, nil
case nfs4.OpRead:
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
var stateid nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(stateid[:], raw)
if status := h.checkStateid(stateid, reg.cur, reg.clientID); status != nfs4.ErrOK {
return nil, status, nil
}
off, err := d.Uint64()
if err != nil {
return nil, 0, err
}
count, err := d.Uint32()
if err != nil {
return nil, 0, err
}
if off > 1<<62 {
return nil, nfs4.ErrInval, nil
}
if uint64(count) > nfs4.DefaultLimits.MaxRead {
count = uint32(nfs4.DefaultLimits.MaxRead)
}
// The fast path reads straight into the reply buffer: no
// intermediate allocation, no second copy, and the end of file
// comes from the descriptor instead of a second attribute call.
if ri, ok := h.FS.(nfsfs.ReadIntoer); ok {
payload := make([]byte, 8, 8+((int(count)+3)&^3))
n, eof, rerr := ri.ReadInto(reg.cur, int64(off), payload[8:cap(payload)])
if rerr != nil {
return nil, mapErr(rerr), nil
}
if eof {
payload[0], payload[1], payload[2], payload[3] = 0, 0, 0, 1
}
binary.BigEndian.PutUint32(payload[4:8], uint32(n))
return payload[:8+((n+3)&^3)], nfs4.ErrOK, nil
}
data, err := h.FS.Read(reg.cur, int64(off), int(count))
if err != nil {
return nil, mapErr(err), nil
}
eof := false
if info, err := h.FS.Getattr(reg.cur); err == nil {
eof = int64(off)+int64(len(data)) >= info.Size
}
return nfs4.AppendReadRes(nil, eof, data), nfs4.ErrOK, nil
case nfs4.OpReaddir:
return h.readdir(d, reg)
case nfs4.OpWrite:
return h.writeOp(d, reg)
case nfs4.OpCreate:
return h.createOp(d, reg, c)
case nfs4.OpRemove:
return h.removeOp(d, reg)
case nfs4.OpRename:
return h.renameOp(d, reg)
case nfs4.OpSetattr:
return h.setattrOp(d, reg)
case nfs4.OpLink:
return h.linkOp(d, reg)
case nfs4.OpReadlink:
return h.readlinkOp(d, reg)
case nfs4.OpCommit:
return h.commitOp(d, reg)
case nfs4.OpSecinfo:
return h.secinfoOp(d, reg)
case nfs4.OpSecinfoNoName:
return h.secinfoNoNameOp(d, reg)
case nfs4.OpExchangeID:
a, err := nfs4.DecodeExchangeIDArgs(d)
if err != nil {
return nil, 0, err
}
clientid, sequence, flags, rebooted := h.sessions().exchangeID(a.Verifier, a.OwnerID, time.Now())
if rebooted != 0 {
// The old life of this owner is gone: its opens, locks,
// delegations and layouts go with it, so the rebooted client
// starts clean and its leftovers deny nobody.
h.dropClientState(rebooted)
}
return nfs4.AppendExchangeIDRes(nil, clientid, sequence, flags, []byte("nfsd")), nfs4.ErrOK, nil
case nfs4.OpCreateSession:
a, err := nfs4.DecodeCreateSessionArgs(d)
if err != nil {
return nil, 0, err
}
id, _, status := h.sessions().createSession(a.ClientID, a.Sequence, a.CBProgram)
if status != nfs4.ErrOK {
return nil, status, nil
}
// A client that offered a back channel binds it to this
// connection: the session remembers the callback program and the
// wire, and CB calls flow through ctx from now on.
if a.Flags&nfs4.CreateSessionFlagConnBackChan != 0 && ctx != nil {
h.sessions().attachCB(id, ctx)
}
// The answer is deterministic in the values it echoes, so a replay
// of this sequence regenerates it byte for byte. The flags are
// echoed as RFC 8881 section 18.36 requires: a client whose
// CONN_BACK_CHAN offer comes back unanswered tears the client
// down instead of mounting. The channels are negotiated down to
// the request: a client rejects a reply larger than what it
// asked for.
return nfs4.AppendCreateSessionRes(nil, id, a.Sequence, a.Flags,
nfs4.NegotiateChannel(a.Fore, nfs4.DefaultForeChannel),
nfs4.NegotiateChannel(a.Back, nfs4.DefaultBackChannel)), nfs4.ErrOK, nil
case nfs4.OpDestroySession:
var id nfs4.SessionID
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(id[:], raw)
// A compound that carries a session may destroy only its own,
// RFC 8881 section 18.37; the operation travels alone in its
// compound, so the unguessable session id itself is the
// credential when no session rode in front.
if sessionClientid != 0 && id.ClientIDOf() != sessionClientid {
return nil, nfs4.ErrBadSession, nil
}
if status := h.sessions().destroySession(id); status != nfs4.ErrOK {
return nil, status, nil
}
// The layouts and directory delegations of the session die with it.
h.layouts().dropSession(id)
h.dirDelegs().dropSession(id)
return nil, nfs4.ErrOK, nil
case nfs4.OpOpen:
return h.openOp(d, reg, c, sessionClientid)
case nfs4.OpClose:
return h.closeOp(d, reg, sessionClientid)
case nfs4.OpLock:
return h.lockOp(d, reg, c, sessionClientid)
case nfs4.OpLockt:
return h.locktOp(d, reg)
case nfs4.OpLocku:
return h.lockuOp(d, reg, sessionClientid)
case nfs4.OpOpenDowngrade:
return h.openDowngradeOp(d, reg, sessionClientid)
case nfs4.OpReleaseLockOwner:
return h.releaseLockOwnerOp(d, sessionClientid)
case nfs4.OpDelegReturn:
return h.delegReturnOp(d, sessionClientid)
case nfs4.OpBackchannelCtl:
return h.backchannelCtlOp(d, ctx)
case nfs4.OpBindConnToSession:
return h.bindConnToSessionOp(d, ctx, sessionClientid)
case nfs4.OpFreeStateid:
return h.freeStateidOp(d, sessionClientid)
case nfs4.OpTestStateid:
return h.testStateidOp(d)
case nfs4.OpSeek:
return h.seekOp(d, reg)
case nfs4.OpAllocate:
return h.rangeOp(d, reg, false)
case nfs4.OpDeallocate:
return h.rangeOp(d, reg, true)
case nfs4.OpIoAdvise:
return h.ioAdviseOp(d, reg)
case nfs4.OpCopy:
return h.copyOp(d, reg)
case nfs4.OpCopyNotify:
return h.copyNotifyOp(d, reg)
case nfs4.OpOffloadCancel:
return h.offloadCancelOp(d, reg)
case nfs4.OpOffloadStatus:
return h.offloadStatusOp(d, reg)
case nfs4.OpClone:
return h.cloneOp(d, reg)
case nfs4.OpLayoutError:
return h.layoutErrorOp(d, reg)
case nfs4.OpLayoutStats:
return h.layoutStatsOp(d, reg)
case nfs4.OpReadPlus:
return h.readPlusOp(d, reg)
case nfs4.OpWriteSame:
return h.writeSameOp(d, reg)
case nfs4.OpGetxattr:
return h.getXattrOp(d, reg)
case nfs4.OpSetxattr:
return h.setXattrOp(d, reg)
case nfs4.OpListxattr:
return h.listXattrOp(d, reg)
case nfs4.OpRemovexattr:
return h.removeXattrOp(d, reg)
case nfs4.OpSetSsv, nfs4.OpWantDelegation:
return nil, nfs4.ErrNotSupp, nil
case nfs4.OpGetDirDelegation:
return h.getDirDelegationOp(d, reg, sessID, sessionClientid)
case nfs4.OpLayoutGet:
return h.layoutGetOp(d, reg, sessID, sessionClientid)
case nfs4.OpLayoutCommit:
return h.layoutCommitOp(d, reg)
case nfs4.OpLayoutReturn:
return h.layoutReturnOp(d, reg, sessID, sessionClientid)
case nfs4.OpGetDeviceInfo:
return h.getDeviceInfoOp(d, ctx)
case nfs4.OpGetDeviceList:
return h.getDeviceListOp(d, ctx)
case nfs4.OpDestroyClientID:
return h.destroyClientIDOp(d, sessionClientid)
case nfs4.OpReclaimComplete:
return h.reclaimCompleteOp(d, sessionClientid)
default:
if op == nfs4.OpIllegal {
return nil, nfs4.ErrOpIllegal, nil
}
return nil, nfs4.ErrNotSupp, nil
}
}
// readdir serves one READDIR page: as many entries as the maxcount budget
// takes, in the backend's order, with the verifier and the cookies the
// client resumes from.
func (h *Handler) readdir(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
cookie, err := d.Uint64()
if err != nil {
return nil, 0, err
}
raw, err := d.Raw(8)
if err != nil {
return nil, 0, err
}
var verifier [8]byte
copy(verifier[:], raw)
dircount, err := d.Uint32()
if err != nil {
return nil, 0, err
}
maxcount, err := d.Uint32()
if err != nil {
return nil, 0, err
}
request, err := nfs4.ReadBitmap(d)
if err != nil {
return nil, 0, err
}
if maxcount < 1024 {
return nil, nfs4.ErrTooSmall, nil
}
page, err := h.FS.ReadDir(reg.cur, cookie, 0)
if err != nil {
return nil, mapErr(err), nil
}
if cookie > 0 && page.Verifier != verifier {
return nil, nfs4.ErrNotSame, nil
}
// The maxcount budget counts every byte of the result body; the
// dircount budget counts the directory information, approximated here
// as the cookie and the name of each entry.
var used, nameUsed int
var entries []nfs4.DirEntryRes
for _, e := range page.Entries {
entry := nfs4.DirEntryRes{Cookie: e.Cookie, Name: e.Name, Attrs: h.attrsOf(e.Handle, e.Info)}
var buf []byte
buf = xdr.AppendBool(buf, true)
buf = xdr.AppendUint64(buf, entry.Cookie)
buf = xdr.AppendString(buf, entry.Name)
buf = nfs4.AppendFattr(buf, request, entry.Attrs)
if used+len(buf)+8 > int(maxcount) ||
(dircount > 0 && nameUsed+len(e.Name)+8 > int(dircount)) {
return nfs4.AppendReadDirRes(nil, page.Verifier, entries, request, false), nfs4.ErrOK, nil
}
used += len(buf)
nameUsed += len(e.Name) + 8
entries = append(entries, entry)
}
return nfs4.AppendReadDirRes(nil, page.Verifier, entries, request, true), nfs4.ErrOK, nil
}
// writeOp serves WRITE. The stateid is validated like every stateful
// write; the answer is always FILE_SYNC with the boot verifier, which
// leaves the client nothing to replay after a restart.
func (h *Handler) writeOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
var stateid nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(stateid[:], raw)
off, err := d.Uint64()
if err != nil {
return nil, 0, err
}
stable, err := d.Uint32()
if err != nil {
return nil, 0, err
}
// The data stays in the request record: the write hands the record's
// own bytes to the backend instead of copying them out first. Raw is
// bounds checked, so a length beyond the record is refused.
dataLen, err := d.Uint32()
if err != nil {
return nil, 0, err
}
dataRaw, err := d.Raw((int(dataLen) + 3) &^ 3)
if err != nil {
return nil, 0, err
}
data := dataRaw[:dataLen]
_ = stable
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
// The stateid names the OPEN the write runs under; the anonymous
// forms are accepted.
if status := h.checkStateid(stateid, reg.cur, reg.clientID); status != nfs4.ErrOK {
return nil, status, nil
}
if off > 1<<62 {
return nil, nfs4.ErrInval, nil
}
n, err := w.Write(reg.cur, int64(off), data)
if err != nil {
return nil, mapErr(err), nil
}
return nfs4.AppendWriteRes(nil, uint32(n), nfs4.StableFileSync, h.writeVerifier()), nfs4.ErrOK, nil
}
// createOp serves CREATE, which in NFSv4 makes everything but a regular
// file: directories, symlinks and the special kinds. The exclusive form is
// answered as its guarded equivalent, which matches it for every case but
// a client retrying with the same verifier.
func (h *Handler) createOp(d *xdr.Decoder, reg *fhreg, c cred) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
kind, err := d.Uint32()
if err != nil {
return nil, 0, err
}
var linkdata string
var major, minor uint32
switch kind {
case nfs4.NF4Lnk:
if linkdata, err = d.String(); err != nil {
return nil, 0, err
}
case nfs4.NF4Blk, nfs4.NF4Chr:
if major, err = d.Uint32(); err != nil {
return nil, 0, err
}
if minor, err = d.Uint32(); err != nil {
return nil, 0, err
}
}
name, err := d.String()
if err != nil {
return nil, 0, err
}
// The object attributes are a full fattr4 of bitmap and list, RFC
// 8881 section 18.4: CREATE carries no create mode union, that is
// the OPEN operation's shape.
request, err := nfs4.ReadBitmap(d)
if err != nil {
return nil, 0, err
}
blob, err := d.VarOpaque()
if err != nil {
return nil, 0, err
}
attrs, derr := nfs4.DecodeFattrAttrs(blob, request)
if derr != nil {
return nil, 0, derr
}
perm := fs.FileMode(attrs.Mode & 0o7777)
if !request.Has(nfs4.AttrMode) {
// No mode named: the server default per kind, a writable
// directory or file.
if kind == nfs4.NF4Dir {
perm = 0o755
} else {
perm = 0o644
}
}
var spec nfsfs.CreateSpec
spec = nfsfs.CreateSpec{Kind: kind, Perm: perm}
spec.LinkData = linkdata
spec.Major, spec.Minor = major, minor
// A fresh object carries the owner of the credential that made it,
// not the daemon's own identity.
spec.Owner = nfsfs.Owner{UID: c.uid, GID: c.gid}
h2, _, err := w.Create(reg.cur, name, spec)
if err != nil {
return nil, mapErr(err), nil
}
dir := reg.cur
reg.cur, reg.haveCur = h2, true
h.notifyDirOf(dir, nfs4.OfBits(nfs4.NotifyAddEntry), name)
return nfs4.AppendCreateRes(nil), nfs4.ErrOK, nil
}
// removeOp serves REMOVE: the named entry of the current directory goes
// away, an empty directory included.
func (h *Handler) removeOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
name, err := d.String()
if err != nil {
return nil, 0, err
}
if err := w.Remove(reg.cur, name); err != nil {
return nil, mapErr(err), nil
}
h.notifyDirOf(reg.cur, nfs4.OfBits(nfs4.NotifyRemoveEntry), name)
return nfs4.AppendRemoveRes(nil), nfs4.ErrOK, nil
}
// renameOp serves RENAME. The standard fixes the roles of the two file
// handles: the saved one, set by SAVEFH, carries the source directory, and
// the current one carries the target directory.
func (h *Handler) renameOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
if reg.saved == nil {
return nil, nfs4.ErrNoFileHandle, nil
}
oldName, err := d.String()
if err != nil {
return nil, 0, err
}
newName, err := d.String()
if err != nil {
return nil, 0, err
}
if err := w.Rename(reg.saved, oldName, reg.cur, newName); err != nil {
return nil, mapErr(err), nil
}
// Both directories see the change: the target gains an entry and the
// source loses one, RFC 8881 section 20.4.
h.notifyDirOf(reg.cur, nfs4.OfBits(nfs4.NotifyAddEntry), newName)
h.notifyDirOf(reg.saved, nfs4.OfBits(nfs4.NotifyRemoveEntry), oldName)
return nfs4.AppendRenameRes(nil), nfs4.ErrOK, nil
}
// setattrOp serves SETATTR: the changes named in the fattr4 are applied to
// the current file. The stateid travels unevaluated: SETATTR is a plain
// backend call. On success the answer names every attribute applied; on
// failure it names none, because the backend applies per call.
func (h *Handler) setattrOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
var stateid nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(stateid[:], raw)
request, err := nfs4.ReadBitmap(d)
if err != nil {
return nil, 0, err
}
blob, err := d.VarOpaque()
if err != nil {
return nil, 0, err
}
updates, err := nfs4.DecodeSetattrBlob(blob, request)
if err != nil {
if errors.Is(err, nfs4.ErrAttrNotSettable) {
return nil, nfs4.ErrAttrNotSupp, nil
}
return nil, 0, err
}
if err := w.Setattr(reg.cur, h.setAttrsOf(updates)); err != nil {
return nil, mapErr(err), nil
}
set := nfs4.Bitmap{}
if updates.HasMode {
set = set.With(nfs4.AttrMode)
}
if updates.HasSize {
set = set.With(nfs4.AttrSize)
}
if updates.UID != nil {
set = set.With(nfs4.AttrOwner)
}
if updates.GID != nil {
set = set.With(nfs4.AttrOwnerGroup)
}
if updates.Atime != nil {
set = set.With(nfs4.AttrTimeAccessSet)
}
if updates.Mtime != nil {
set = set.With(nfs4.AttrTimeModifySet)
}
return nfs4.AppendSetattrRes(nil, set), nfs4.ErrOK, nil
}
// setAttrsOf converts the wire updates into the backend's shape.
func (h *Handler) setAttrsOf(u nfs4.SetAttrUpdates) nfsfs.SetAttrs {
s := nfsfs.SetAttrs{}
if u.HasMode {
mode := u.Mode
s.Mode = &mode
}
if u.HasSize {
size := int64(u.Size)
s.Size = &size
}
s.UID, s.GID = u.UID, u.GID
if u.Atime != nil {
s.Atime = &nfsfs.TimeSet{Now: u.Atime.Server, Time: timeOfNfs(u.Atime.Time)}
}
if u.Mtime != nil {
s.Mtime = &nfsfs.TimeSet{Now: u.Mtime.Server, Time: timeOfNfs(u.Mtime.Time)}
}
return s
}
func timeOfNfs(t nfs4.NfsTime) time.Time {
return time.Unix(t.Seconds, int64(t.Nseconds))
}
// linkOp serves LINK: a hard link named newname lands in the current
// directory and points at the object of the saved file handle.
func (h *Handler) linkOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
if reg.saved == nil {
return nil, nfs4.ErrNoFileHandle, nil
}
name, err := d.String()
if err != nil {
return nil, 0, err
}
if _, _, err := w.Link(reg.saved, reg.cur, name); err != nil {
return nil, mapErr(err), nil
}
return nfs4.AppendLinkRes(nil), nfs4.ErrOK, nil
}
// readlinkOp serves READLINK: the target text of the symlink in the
// current handle. A handle that names anything else is NFS4ERR_INVAL.
func (h *Handler) readlinkOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
target, err := h.FS.ReadLink(reg.cur)
if err != nil {
return nil, mapErr(err), nil
}
return nfs4.AppendReadlinkRes(nil, target), nfs4.ErrOK, nil
}
// commitOp serves COMMIT: the backend's dirty data is flushed to stable
// storage and the answer carries the boot verifier, so the client knows
// the flushed writes are the ones it made against this server life.
func (h *Handler) commitOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
// The offset, the count and the client's write verifier name the range
// of the flush; this backend is synchronous, so the whole file is
// stable whenever COMMIT runs and the range is not evaluated.
if _, err := d.Uint64(); err != nil {
return nil, 0, err
}
if _, err := d.Uint32(); err != nil {
return nil, 0, err
}
if _, err := d.Raw(8); err != nil {
return nil, 0, err
}
if err := w.Sync(reg.cur); err != nil {
return nil, mapErr(err), nil
}
return nfs4.AppendCommitRes(nil, h.writeVerifier()), nfs4.ErrOK, nil
}
// acceptedSecInfo is the SECINFO answer of this server: the flavours it
// accepts for every name, AUTH_SYS among them. The answer includes
// names that do not exist, because the operation exists precisely so a
// client can probe before it picks its credential.
var acceptedSecInfo = []nfs4.SecinfoEntry{
{Flavor: nfs4.SecFlavorSys},
}
// secinfoAnswer is shared by both SECINFO operations.
func secinfoAnswer() []byte {
return nfs4.AppendSecinfoRes(nil, acceptedSecInfo)
}
// secinfoOp serves SECINFO for an explicit name under the current
// directory.
func (h *Handler) secinfoOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
name, err := d.String()
if err != nil {
return nil, 0, err
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
if err := nfsfs.ValidName(name); err != nil {
return nil, mapErr(err), nil
}
return secinfoAnswer(), nfs4.ErrOK, nil
}
// secinfoNoNameOp serves SECINFO_NO_NAME. The argument is the
// secinfo_style4 enum alone, RFC 8881 section 18.44: style
// StyleCurrentFH answers for the current file handle, StyleParent for
// its parent directory. The answer carries no name on the wire.
func (h *Handler) secinfoNoNameOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
style, err := nfs4.DecodeSecinfoNoNameArgs(d)
if err != nil {
if errors.Is(err, nfs4.ErrBadStyle) {
return nil, nfs4.ErrInval, nil
}
return nil, 0, err
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
switch style {
case nfs4.StyleCurrentFH, nfs4.StyleParent:
return secinfoAnswer(), nfs4.ErrOK, nil
default:
return nil, nfs4.ErrInval, nil
}
}
// attrsOf builds the protocol attributes of one file from the backend
// info. The change attribute is the modification time in nanoseconds: the
// finest change counter a local directory offers without extra state, and
// the same clock the access and modify times report.
func (h *Handler) attrsOf(fh nfsfs.Handle, info nfsfs.Info) nfs4.Attrs {
a := nfs4.Attrs{
Type: typeOf(info),
FHExpireType: nfs4.FH4Persistent,
Change: uint64(info.ModTime.UnixNano()),
Size: uint64(info.Size),
LinkSupport: true,
SymlinkSupport: true,
NamedAttr: false,
FSID: [2]uint64{info.Dev, 0},
UniqueHandles: true,
FileHandle: fh,
FileID: info.Ino,
Mode: uint32(info.Mode.Perm()),
Numlinks: uint32(info.Nlink),
Owner: strconv.FormatUint(uint64(info.UID), 10),
OwnerGroup: strconv.FormatUint(uint64(info.GID), 10),
SpaceUsed: uint64(info.Size),
TimeAccess: nfsTimeOf(info.ModTime),
TimeMetadata: nfsTimeOf(info.ModTime),
TimeModify: nfsTimeOf(info.ModTime),
MountedOnFileID: info.Ino,
Limits: nfs4.DefaultLimits,
}
return a
}
func nfsTimeOf(t time.Time) nfs4.NfsTime {
return nfs4.NfsTimeOf(t.Unix(), uint32(t.Nanosecond()))
}
// typeOf maps a Go file mode onto the protocol file type.
func typeOf(info nfsfs.Info) uint32 {
m := info.Mode
switch {
case m&fs.ModeDir != 0:
return nfs4.NF4Dir
case m&fs.ModeSymlink != 0:
return nfs4.NF4Lnk
case m&fs.ModeDevice != 0 && m&fs.ModeCharDevice != 0:
return nfs4.NF4Chr
case m&fs.ModeDevice != 0:
return nfs4.NF4Blk
case m&fs.ModeNamedPipe != 0:
return nfs4.NF4Fifo
case m&fs.ModeSocket != 0:
return nfs4.NF4Sock
default:
return nfs4.NF4Reg
}
}
// mapErr turns a backend error into the protocol status it names.
func mapErr(err error) uint32 {
switch {
case err == nil:
return nfs4.ErrOK
case errors.Is(err, nfsfs.ErrNoEnt):
return nfs4.ErrNoEnt
case errors.Is(err, nfsfs.ErrNotDir):
return nfs4.ErrNotDir
case errors.Is(err, nfsfs.ErrIsDir):
return nfs4.ErrIsDir
case errors.Is(err, nfsfs.ErrStale):
return nfs4.ErrStale
case errors.Is(err, nfsfs.ErrNameTooLong):
return nfs4.ErrNameTooLong
case errors.Is(err, nfsfs.ErrBadName):
return nfs4.ErrBadName
case errors.Is(err, nfsfs.ErrPermission):
return nfs4.ErrAccess
case errors.Is(err, nfsfs.ErrReadOnly):
return nfs4.ErrROFS
case errors.Is(err, nfsfs.ErrExist):
return nfs4.ErrExist
case errors.Is(err, nfsfs.ErrNoSpace):
return nfs4.ErrNoSpc
case errors.Is(err, nfsfs.ErrNotEmpty):
return nfs4.ErrNotEmpty
case errors.Is(err, nfsfs.ErrInval):
return nfs4.ErrInval
case errors.Is(err, nfsfs.ErrNotLnk):
return nfs4.ErrInval
case errors.Is(err, nfsfs.ErrIO):
return nfs4.ErrIO
default:
return nfs4.ErrServerFault
}
}
// probeCount is a test hook: a number that changes between calls, used by
// the tests to build distinct client identities.
func (h *Handler) probeCount() int {
h.mu.Lock()
defer h.mu.Unlock()
h.probes++
return h.probes
}
// openOp serves OPEN: it opens or creates a regular file under the
// current directory, records the share reservation and hands the client
// the stateid every stateful use of the file will carry. Served claims
// are CLAIM_NULL, CLAIM_PREVIOUS, CLAIM_FH, CLAIM_DELEGATE_CUR and
// CLAIM_DELEGATE_CUR_FH; the exclusive creation forms replay by their
// verifier and answer EXIST on a fresh verifier. A create whose
// attributes name size 0 truncates the existing file.
// exclVerifier answers the create verifier stored for the last
// exclusive create of the name under the directory, if any.
func (h *Handler) exclVerifier(dirKey, name string) ([8]byte, bool) {
h.exclMu.Lock()
defer h.exclMu.Unlock()
if h.excl == nil {
return [8]byte{}, false
}
verf, ok := h.excl[dirKey+"|"+name]
return verf, ok
}
// setExclVerifier remembers the create verifier of a fresh exclusive
// create, so a client retrying after a lost reply replays into success
// instead of EXIST, RFC 8881 section 18.16. The map is in memory only:
// a restart drops it together with every other live state.
func (h *Handler) setExclVerifier(dirKey, name string, verf [8]byte) {
h.exclMu.Lock()
defer h.exclMu.Unlock()
if h.excl == nil {
h.excl = make(map[string][8]byte)
}
h.excl[dirKey+"|"+name] = verf
}
func (h *Handler) openOp(d *xdr.Decoder, reg *fhreg, c cred, sessionClientid uint64) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
a, err := nfs4.DecodeOpenArgs(d)
if err != nil {
if errors.Is(err, nfs4.ErrNotSuppName) {
return nil, nfs4.ErrNotSupp, nil
}
return nil, 0, err
}
if a.Previous {
// CLAIM_PREVIOUS: the client reclaims an open it held before the
// restart. Only valid inside the grace window, before the client
// announced RECLAIM_COMPLETE, and against a state the store
// loaded back.
if ok, gstatus := h.graced().reclaimOKFor(sessionClientid, time.Now()); !ok {
return nil, gstatus, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
stateid, status := h.openStates().reclaimOpen(reg.cur, sessionClientid)
if status != nfs4.ErrOK {
return nil, status, nil
}
return nfs4.AppendOpenResDeleg(nil, stateid, nfs4.OpenDelegNone, nfs4.Stateid{}), nfs4.ErrOK, nil
}
switch a.Claim {
case nfs4.ClaimDelegatePrev, nfs4.ClaimDelegatePrevFh:
// The v4.0 delegation reclaim claims name state this server does
// not track; nothing sane can be answered for them.
return nil, nfs4.ErrNotSupp, nil
}
// Resolve the file the open names. The file handle claims carry no
// name: the current file handle is the file, nothing is created and
// RFC 8881 section 18.16 leaves it in place. Only CLAIM_NULL may
// create; a delegation claim names a file the client already holds.
// A named open walks the current directory, which the credential
// must be allowed to search; the file handle claims touch no
// directory, and the file's own access is checked below.
if a.Claim == nfs4.ClaimNull || a.Claim == nfs4.ClaimDelegateC {
if status := h.authoriseHandle(reg.cur, nfsfs.AccessLookup, c); status != nfs4.ErrOK {
return nil, status, nil
}
}
var fh nfsfs.Handle
var created bool
switch a.Claim {
case nfs4.ClaimFH, nfs4.ClaimDelegateCFh:
info, gerr := h.FS.Getattr(reg.cur)
if gerr != nil {
return nil, mapErr(gerr), nil
}
if info.Mode.IsDir() {
return nil, nfs4.ErrIsDir, nil
}
fh = reg.cur
default:
create := a.Create && a.Claim == nfs4.ClaimNull
dirKey := fileKey(reg.cur)
perm := fs.FileMode(a.Perm & 0o777)
// An exclusive create without an attribute set names no mode; the
// file gets the server default rather than no permission bits.
if a.Exclusive && perm == 0 {
perm = 0o644
}
fh, _, created, err = w.Open(reg.cur, a.Name, create, a.Guarded, a.Truncate, perm,
nfsfs.Owner{UID: c.uid, GID: c.gid})
if err != nil {
// An exclusive create over an existing name answers EXIST,
// unless the verifier replays the one this server stored for
// the lost reply of the very create, RFC 8881 section 18.16.
if a.Exclusive && errors.Is(err, nfsfs.ErrExist) {
if verf, ok := h.exclVerifier(dirKey, a.Name); ok && verf == a.ExclusiveVerf {
fh, _, created, err = w.Open(reg.cur, a.Name, false, false, false, perm,
nfsfs.Owner{UID: c.uid, GID: c.gid})
}
}
if err != nil {
return nil, mapErr(err), nil
}
}
if a.Exclusive && created {
h.setExclVerifier(dirKey, a.Name, a.ExclusiveVerf)
}
}
// The opened file must grant the share access the client asked for;
// a create already carried the directory's modify right through the
// gate.
if a.Access&nfs4.ShareAccessRead != 0 {
if status := h.authoriseHandle(fh, nfsfs.AccessRead, c); status != nfs4.ErrOK {
return nil, status, nil
}
}
if a.Access&nfs4.ShareAccessWrite != 0 {
if status := h.authoriseHandle(fh, nfsfs.AccessModify, c); status != nfs4.ErrOK {
return nil, status, nil
}
}
// A named open replaces the current file handle with the opened file,
// RFC 8881 section 18.16; a file handle claim already holds it.
reg.cur, reg.haveCur = fh, true
// A delegation held by another client conflicts with this open: the
// recall travels over the holder's back channel on the connection's
// callback worker, and this open answers NFS4ERR_DELAY while the
// recall runs, as RFC 8881 section 18.16 prescribes for a conflicting
// open. The retry after the recall sees the file free.
key := fileKey(fh)
if existing, ok := h.delegs().holder(key); ok && existing.clientID != sessionClientid {
h.queueRecall(existing.sessID, existing.stateid, key, fh)
return nil, nfs4.ErrDelay, nil
}
st, status := h.openStates().open(fh, sessionClientid, a.Owner, a.Access, a.Deny)
if status != nfs4.ErrOK {
return nil, status, nil
}
h.openStates().persist(h.StateDir)
// The delegation is granted when this is the file's only open: read
// only opens carry a read delegation, write opens a write one. The
// delegation belongs to the session that opened the file, which is
// where its recalls travel. A delegation claim merges an open into a
// delegation the client already holds, so no second one is minted.
// The delegation is granted when this is the file's only open: read
// only opens carry a read delegation, write opens a write one. The
// delegation belongs to the session that opened the file, which is
// where its recalls travel. A delegation claim joins the open to the
// delegation the client already holds on the file, identified by the
// client and the file alone, so no second one is minted, RFC 8881
// section 18.16.4.
delegType, delegSt := uint32(nfs4.OpenDelegNone), nfs4.Stateid{}
delegClaim := a.Claim == nfs4.ClaimDelegateC || a.Claim == nfs4.ClaimDelegateCFh
if h.openStates().countOpens(fh) == 1 && !delegClaim {
if a.Access == nfs4.ShareAccessRead {
delegType = nfs4.OpenDelegRead
} else if a.Access&nfs4.ShareAccessWrite != 0 {
delegType = nfs4.OpenDelegWrite
}
if delegType != nfs4.OpenDelegNone {
if sessID, ok := h.sessions().sessionOfClient(sessionClientid); ok {
if delegSt, status = h.delegs().grant(sessID, sessionClientid, key, delegType); status != nfs4.ErrOK {
delegType = nfs4.OpenDelegNone
}
}
}
if delegType != nfs4.OpenDelegNone {
h.openStates().bindDelegation(st, delegSt)
}
}
return nfs4.AppendOpenResDeleg(nil, st, delegType, delegSt), nfs4.ErrOK, nil
}
// closeOp serves CLOSE: the share reservation ends and the stateid the
// answer carries is dead on arrival. An OPEN with byte range locks still
// held is refused with NFS4ERR_LOCKS_HELD.
func (h *Handler) closeOp(d *xdr.Decoder, reg *fhreg, sessionClientid uint64) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
if h.locks().locksHeldOn(reg.cur) {
return nil, nfs4.ErrLocksHeld, nil
}
// The CLOSE arguments carry the v4.0 seqid ahead of the stateid; it
// is deprecated and ignored, but it is on the wire, RFC 8881 section
// 18.2.3.
if _, err := d.Uint32(); err != nil {
return nil, 0, err
}
var st nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(st[:], raw)
closed, status := h.openStates().close(st, sessionClientid)
if status != nfs4.ErrOK {
return nil, status, nil
}
h.openStates().persist(h.StateDir)
// An OPEN whose delegation was granted dies with the open: the last
// open of the file takes the delegation with it.
if h.openStates().countOpens(reg.cur) == 0 {
h.delegs().revoke(fileKey(reg.cur))
}
return nfs4.AppendCloseRes(nil, closed), nfs4.ErrOK, nil
}
// checkStateid validates a stateid a stateful operation carries. The
// anonymous forms, all zero and all ones, are accepted without state;
// a real stateid routes by its family mark to the store that minted
// it: OPEN to the open store, DELE to the delegation store, whose
// stateid RFC 8881 section 10.3 lets the client present for its data
// operations. Every stateid must belong to the asking client.
func (h *Handler) checkStateid(st nfs4.Stateid, fh nfsfs.Handle, clientid uint64) uint32 {
if string(st[4:8]) == "DELE" {
return h.delegs().checkDataStateid(st, fh, clientid)
}
_, status := h.openStates().checkStateid(st, fh, clientid)
return status
}
// lockOp serves LOCK: a byte range lock hung from an OPEN, either by a new
// lock owner carrying its open stateid, or by an existing lock stateid.
// The locks live beside the share reservations the server tracks; a
// reclaim outside the grace window is refused.
func (h *Handler) lockOp(d *xdr.Decoder, reg *fhreg, c cred, sessionClientid uint64) ([]byte, uint32, error) {
w := h.writer()
if w == nil {
return nil, nfs4.ErrROFS, nil
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
lockType, err := d.Uint32()
if err != nil {
return nil, 0, err
}
if lockType != nfs4.LockTypeRead && lockType != nfs4.LockTypeWrite {
return nil, nfs4.ErrInval, nil
}
reclaim, err := d.Bool()
if err != nil {
return nil, 0, err
}
offset, err := d.Uint64()
if err != nil {
return nil, 0, err
}
length, err := d.Uint64()
if err != nil {
return nil, 0, err
}
newOwner, err := d.Bool()
if err != nil {
return nil, 0, err
}
var lockClientid uint64
var lockOwner []byte
if newOwner {
// The locker union carries the open stateid the lock hangs from
// and the identity of the lock owner: open_seqid, open_stateid,
// lock_seqid, clientid, owner. The open stateid must name a live
// open of this file that belongs to the asking client, and the
// lock is registered under the session's client, never under a
// clientid the wire alone claims, RFC 8881 section 18.10.
if _, err = d.Uint32(); err != nil {
return nil, 0, err
}
raw, rerr := d.Raw(16)
if rerr != nil {
return nil, 0, rerr
}
var lockSt nfs4.Stateid
copy(lockSt[:], raw)
if _, err = d.Uint32(); err != nil {
return nil, 0, err
}
if _, err = d.Uint64(); err != nil { // locker4 open owner: clientid
return nil, 0, err
}
if lockOwner, err = d.VarOpaque(); err != nil {
return nil, 0, err
}
if _, status := h.openStates().checkStateid(lockSt, reg.cur, sessionClientid); status != nfs4.ErrOK {
return nil, nfs4.ErrBadStateid, nil
}
lockClientid = sessionClientid
} else {
// An existing lock owner: the stateid names the lock state.
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
var lockSt nfs4.Stateid
copy(lockSt[:], raw)
if _, err = d.Uint32(); err != nil {
return nil, 0, err
}
ls, status := h.locks().byStateid(lockSt, reg.cur, sessionClientid)
if status != nfs4.ErrOK {
return nil, status, nil
}
lockClientid, lockOwner = ls.clientID, ls.owner
}
// A reclaim re-establishes a lock held before the restart. The server
// recovers no lock state itself, so inside the grace window the
// reclaim re-registers the lock from the presented identity; after the
// window it is NO_GRACE (RFC 8881 section 13.12).
if reclaim && !h.graced().active(time.Now()) {
return nil, nfs4.ErrNoGrace, nil
}
st, status := h.locks().lock(reg.cur, lockClientid, lockOwner,
lockType == nfs4.LockTypeWrite, offset, length)
if status == nfs4.ErrDenied {
// A denied lock is remembered, so the release of the conflicting
// range can notify this owner over its back channel. The routing
// identity is the session client, the wire owner names the lock
// owner within it.
h.locks().addWaiter(reg.cur, sessionClientid, lockOwner, offset, length,
lockType == nfs4.LockTypeWrite)
return nil, status, nil
}
if status != nfs4.ErrOK {
return nil, status, nil
}
return nfs4.AppendLockRes(nil, st), nfs4.ErrOK, nil
}
// locktOp serves LOCKT serves LOCKT serves LOCKT: a probe whether a lock over the range would
// conflict with another owner's locks on the current file. A conflict is
// answered NFS4ERR_DENIED with the holder of the lock.
func (h *Handler) locktOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
lockType, err := d.Uint32()
if err != nil {
return nil, 0, err
}
if lockType != nfs4.LockTypeRead && lockType != nfs4.LockTypeWrite {
return nil, nfs4.ErrInval, nil
}
offset, err := d.Uint64()
if err != nil {
return nil, 0, err
}
length, err := d.Uint64()
if err != nil {
return nil, 0, err
}
clientid, err := d.Uint64()
if err != nil {
return nil, 0, err
}
owner, err := d.VarOpaque()
if err != nil {
return nil, 0, err
}
denied, status := h.locks().test(reg.cur, clientid, owner,
lockType == nfs4.LockTypeWrite, offset, length)
if status == nfs4.ErrDenied {
return nfs4.AppendLocktResDenied(nil, denied.Offset, denied.Length,
denied.LockType, denied.ClientID, denied.Owner), nfs4.ErrDenied, nil
}
if status != nfs4.ErrOK {
return nil, status, nil
}
return nfs4.AppendLocktResOK(nil), nfs4.ErrOK, nil
}
// lockuOp serves LOCKU: the release of one range of a lock stateid. The
// answer carries the stateid with a bumped sequence.
func (h *Handler) lockuOp(d *xdr.Decoder, reg *fhreg, sessionClientid uint64) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
if _, err := d.Uint32(); err != nil { // lock type, echoed
return nil, 0, err
}
if _, err := d.Uint32(); err != nil { // seqid, deprecated
return nil, 0, err
}
var st nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(st[:], raw)
offset, err := d.Uint64()
if err != nil {
return nil, 0, err
}
length, err := d.Uint64()
if err != nil {
return nil, 0, err
}
closed, status := h.locks().unlock(st, sessionClientid, offset, length)
if status != nfs4.ErrOK {
return nil, status, nil
}
// Waiters whose conflict the release may have lifted learn about it
// over their back channel; the notification is advisory.
for _, w := range h.locks().takeWaiters(reg.cur, offset, length) {
h.notifyLockAvailable(reg.cur, w.clientID, w.owner)
}
return nfs4.AppendLockuRes(nil, closed), nfs4.ErrOK, nil
}
// notifyLockAvailable pushes CB_NOTIFY_LOCK to the session of the lock
// owner a denied lock was recorded for. The notification is queued onto
// the connection's callback worker, so a LOCKU served on the waiter's
// own connection never waits for the reply only that connection's read
// loop can route.
func (h *Handler) notifyLockAvailable(fh nfsfs.Handle, clientid uint64, owner []byte) {
sessID, ok := h.sessions().sessionOfClient(clientid)
if !ok {
return
}
args := nfs4.AppendCBNotifyLockArgs(nil, fh, clientid, owner)
_ = h.queueCB(sessID, "lock-notify", [][]byte{args})
}
// openDowngradeOp serves OPEN_DOWNGRADE: the share access and deny bits of
// a live OPEN narrow down and the stateid sequence moves one up.
func (h *Handler) openDowngradeOp(d *xdr.Decoder, reg *fhreg, sessionClientid uint64) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
// The deprecated v4.0 seqid rides ahead of the stateid, RFC 8881
// section 18.7.3.
if _, err := d.Uint32(); err != nil {
return nil, 0, err
}
var st nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(st[:], raw)
access, err := d.Uint32()
if err != nil {
return nil, 0, err
}
deny, err := d.Uint32()
if err != nil {
return nil, 0, err
}
closed, status := h.openStates().downgrade(st, sessionClientid, access, deny)
if status != nfs4.ErrOK {
return nil, status, nil
}
_ = closed
return nfs4.AppendOpenDowngradeRes(nil), nfs4.ErrOK, nil
}
// destroyClientIDOp serves DESTROY_CLIENTID: the client, its sessions
// and its OPEN state go away. A compound that carries a session may
// destroy only its own client, RFC 8881 section 18.50; one without a
// session authenticates by holding the unguessable client id. A client
// id the server does not know is NFS4ERR_STALE_CLIENTID.
func (h *Handler) destroyClientIDOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) {
clientid, err := d.Uint64()
if err != nil {
return nil, 0, err
}
if sessionClientid != 0 && clientid != sessionClientid {
return nil, nfs4.ErrClientIDBusy, nil
}
if !h.sessions().destroyClientID(clientid) {
return nil, nfs4.ErrStaleClientID, nil
}
h.dropClientState(clientid)
return nil, nfs4.ErrOK, nil
}
// reclaimCompleteOp serves RECLAIM_COMPLETE: the client announces it has
// reclaimed everything it could. A second announcement is
// NFS4ERR_COMPLETE_ALREADY, and an announcement after the grace window
// closed is NFS4ERR_NO_GRACE.
func (h *Handler) reclaimCompleteOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) {
oneClient, err := d.Bool()
if err != nil {
return nil, 0, err
}
clientid := sessionClientid
if oneClient {
// The rca_one_client form names the client on behalf of another;
// this build answers it for the caller alone, which is the only
// client it may speak for.
if _, err = d.Uint64(); err != nil {
return nil, 0, err
}
}
if !h.sessions().knownClient(clientid) {
return nil, nfs4.ErrStaleClientID, nil
}
if !h.graced().active(time.Now()) {
return nil, nfs4.ErrNoGrace, nil
}
if !h.graced().complete(clientid, time.Now()) {
return nil, nfs4.ErrCompleteAlready, nil
}
return nil, nfs4.ErrOK, nil
}
// SendCB delivers one CB_COMPOUND to the session's back channel on the
// connection's callback worker and waits for the reply. It serves
// callers on their own goroutines; the request path uses queueRecall
// and queueCB, which never block the dispatching connection.
func (h *Handler) SendCB(sessID nfs4.SessionID, tag string, ops [][]byte) (nfs4.CompoundRes, [][]byte, error) {
return h.sessions().callCB(sessID, tag, ops)
}
// queueCB posts one fire and forget CB_COMPOUND to the session's back
// channel.
func (h *Handler) queueCB(sessID nfs4.SessionID, tag string, ops [][]byte) error {
return h.sessions().queueCB(sessID, tag, ops, nil)
}
// queueRecall recalls one delegation over the holder's back channel
// without blocking the caller: the recall, and the revocation that
// follows it whatever the delivery outcome, run on the holder's
// connection worker. A recall that cannot even be queued, because the
// session or its back channel is gone, revokes at once; the revocation
// lands only on the very delegation the recall named, so a grant that
// arrived in the meantime survives.
func (h *Handler) queueRecall(sessID nfs4.SessionID, st nfs4.Stateid, key string, fh nfsfs.Handle) {
recall := [][]byte{nfs4.AppendCBRecallArgs(nil, st, false, fh)}
err := h.sessions().queueCB(sessID, "recall", recall, func(cbResult) {
h.delegs().revokeIf(key, st)
})
if err != nil {
h.delegs().revokeIf(key, st)
}
}
// dropClientState releases every piece of state a client holds: its
// opens, locks, delegations, layouts and directory delegations. It is
// what DESTROY_CLIENTID, a rebooting EXCHANGE_ID and a lapsed lease
// require, so none of them leaves state behind under an identity that
// never comes back.
func (h *Handler) dropClientState(clientid uint64) {
h.openStates().dropClient(clientid)
h.locks().dropClient(clientid)
h.delegs().dropClient(clientid)
h.layouts().dropClient(clientid)
h.dirDelegs().dropClient(clientid)
}