Files
nfs/internal/nfs4server/dirdeleg.go
T
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

208 lines
5.8 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Directory delegations: the store of granted directory delegations, the
// GET_DIR_DELEGATION handler and the notification the server pushes to a
// deleg holder when the directory changes under it.
package nfs4server
import (
"sync"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// A dirDeleg is one granted directory delegation.
type dirDeleg struct {
stateid nfs4.Stateid
sessID nfs4.SessionID
clientid uint64
fh nfsfs.Handle
types nfs4.Bitmap
}
// dirDelegStore tracks the live directory delegations. A directory
// carries at most one.
type dirDelegStore struct {
mu sync.Mutex
next uint64
byDir map[string]*dirDeleg
}
func newDirDelegStore() *dirDelegStore {
return &dirDelegStore{byDir: make(map[string]*dirDeleg)}
}
// grant issues the directory delegation; a second client asking for the
// same directory is denied, the same client receives its live stateid.
func (s *dirDelegStore) grant(sessID nfs4.SessionID, clientid uint64, fh nfsfs.Handle) (*dirDeleg, uint32) {
s.mu.Lock()
defer s.mu.Unlock()
if d, ok := s.byDir[fileKey(fh)]; ok {
if d.clientid == clientid {
return d, nfs4.ErrOK
}
return nil, nfs4.ErrDenied
}
s.next++
var st nfs4.Stateid
setStateidSeq(&st, 1)
copy(st[4:], "DDEL")
for i := range 8 {
st[15-i] = byte(s.next >> (8 * i))
}
d := &dirDeleg{stateid: st, sessID: sessID, clientid: clientid, fh: fh}
s.byDir[fileKey(fh)] = d
return d, nfs4.ErrOK
}
// dropClient and dropSession remove the delegations that die with the
// client or the session.
func (s *dirDelegStore) dropClient(clientid uint64) {
s.mu.Lock()
defer s.mu.Unlock()
for key, d := range s.byDir {
if d.clientid == clientid {
delete(s.byDir, key)
}
}
}
func (s *dirDelegStore) dropSession(sessID nfs4.SessionID) {
s.mu.Lock()
defer s.mu.Unlock()
for key, d := range s.byDir {
if d.sessID == sessID {
delete(s.byDir, key)
}
}
}
// count reports how many directory delegations are live.
func (s *dirDelegStore) count() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.byDir)
}
// dirDelegs returns the directory delegation store, made once per
// handler.
func (h *Handler) dirDelegs() *dirDelegStore {
h.mu.Lock()
defer h.mu.Unlock()
if h.dirDelegSt == nil {
h.dirDelegSt = newDirDelegStore()
}
return h.dirDelegSt
}
// getDirDelegationOp serves GET_DIR_DELEGATION over the current
// directory: the stateid hangs from an open of the directory and the
// notification types are echoed back, this server can produce them all.
func (h *Handler) getDirDelegationOp(d *xdr.Decoder, reg *fhreg, sessID nfs4.SessionID, clientid uint64) ([]byte, uint32, error) {
signalAvail, err := d.Bool()
if err != nil {
return nil, 0, err
}
notifyTypes, err := nfs4.ReadBitmap(d)
if err != nil {
return nil, 0, err
}
for range 2 {
if _, err = d.Int64(); err != nil { // attr delay seconds
return nil, 0, err
}
if _, err = d.Uint32(); err != nil { // attr delay nseconds
return nil, 0, err
}
}
if _, err = nfs4.ReadBitmap(d); err != nil { // child attributes
return nil, 0, err
}
if _, err = nfs4.ReadBitmap(d); err != nil { // dir attributes
return nil, 0, err
}
_ = signalAvail
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
// Only directories carry directory delegations.
if info, gerr := h.FS.Getattr(reg.cur); gerr == nil && !info.IsDir() {
return nil, nfs4.ErrWrongType, nil
}
dg, status := h.dirDelegs().grant(sessID, clientid, reg.cur)
if status != nfs4.ErrOK {
return nil, status, nil
}
verf := h.writeVerifier()
return nfs4.AppendGetDirDelegationResOK(nil, verf, dg.stateid, notifyTypes, nfs4.Bitmap{}, nfs4.Bitmap{}), nfs4.ErrOK, nil
}
// notifyDir pushes one CB_NOTIFY to the holder of the directory
// delegation, if the changed directory is delegated to somebody else.
// The notification is advisory and queued onto the holder's callback
// worker, so a mutation served on the holder's own connection never
// waits for the reply only that connection's read loop can route.
func (h *Handler) notifyDir(dir nfsfs.Handle, change nfs4.Notify4) {
dd, ok := h.dirDelegByFH(dir)
if !ok {
return
}
args := nfs4.AppendCBNotifyArgs(nil, dd.stateid, dir, []nfs4.Notify4{change})
_ = h.queueCB(dd.sessID, "dir-notify", [][]byte{args})
}
// dirDelegByFH finds the live delegation of a directory.
func (h *Handler) dirDelegByFH(dir nfsfs.Handle) (*dirDeleg, bool) {
h.mu.Lock()
store := h.dirDelegSt
h.mu.Unlock()
if store == nil {
return nil, false
}
store.mu.Lock()
defer store.mu.Unlock()
d, ok := store.byDir[fileKey(dir)]
return d, ok
}
// notifyDirOf is the helper the mutating operations call: it reads the
// parent through the backend and pushes the change. nameCookie carries
// the cookie semantics of the event; zero suffices for the wire shape
// this build answers with.
func (h *Handler) notifyDirOf(dir nfsfs.Handle, mask nfs4.Bitmap, name string) {
vals := buildNotifyBody(mask, name)
if vals == nil {
return
}
h.notifyDir(dir, nfs4.Notify4{Mask: mask, Vals: vals})
}
// buildNotifyBody encodes the change body the mask names.
func buildNotifyBody(mask nfs4.Bitmap, name string) []byte {
if mask.Has(nfs4.NotifyAddEntry) {
return nfs4.AppendNotifyAdd(nil, name, 0, true)
}
if mask.Has(nfs4.NotifyRemoveEntry) {
return nfs4.AppendNotifyRemove(nil, name, 0)
}
return nil
}
// notifyAppendCB walks the decoded notify body for the tests: it splits
// the name out of a notify_entry4 payload.
func notifyNameOf(body []byte) string {
d := xdr.NewDecoder(body)
if _, err := d.Uint32(); err != nil { // nad_old_entry count
return ""
}
name, err := d.String()
if err != nil {
return ""
}
return name
}