feat: full NFSv4.2 server and client in pure Go
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

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 18:51:17 +02:00
commit a9b8039ef7
153 changed files with 34403 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The handlers of the remaining NFSv4.1 operations: the parent lookup,
// the attribute comparisons, the lock owner retirement and the session
// binding family.
package nfs4server
import (
"errors"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// maxOps bounds the TEST_STATEID array, matching the COMPOUND bound of
// the wire codecs.
const maxOps = 1024
// lookuppOp serves LOOKUPP: the current handle becomes its parent
// directory.
func (h *Handler) lookuppOp(reg *fhreg) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
parent, _, err := h.FS.Parent(reg.cur)
if err != nil {
return nil, mapErr(err), nil
}
reg.cur, reg.haveCur = parent, true
return nil, nfs4.ErrOK, nil
}
// verifyOp serves VERIFY and NVERIFY: the compound proceeds only when the
// file carries, or with the negated form does not carry, every attribute
// the arguments assert. A value this build cannot compare answers
// ATTRNOTSUPP.
func (h *Handler) verifyOp(d *xdr.Decoder, reg *fhreg, negate bool) ([]byte, uint32, error) {
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
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 err == nfs4.ErrAttrNotSettable {
return nil, nfs4.ErrAttrNotSupp, nil
}
return nil, 0, err
}
info, ferr := h.FS.Getattr(reg.cur)
if ferr != nil {
return nil, mapErr(ferr), nil
}
mismatch := false
switch {
case updates.HasMode && uint32(info.Mode.Perm()) != updates.Mode:
mismatch = true
case updates.HasSize && uint64(info.Size) != updates.Size:
mismatch = true
case updates.UID != nil && info.UID != *updates.UID:
mismatch = true
case updates.GID != nil && info.GID != *updates.GID:
mismatch = true
case updates.Atime != nil && !nfsTimeMatches(updates.Atime, info):
mismatch = true
case updates.Mtime != nil && !nfsTimeMatches(updates.Mtime, info):
mismatch = true
}
if mismatch == negate {
return nil, nfs4.ErrOK, nil
}
if negate {
return nil, nfs4.ErrSame, nil
}
return nil, nfs4.ErrNotSame, nil
}
// nfsTimeMatches reports whether the asserted time is the file time the
// backend reports. A server time assertion never matches a fixed value;
// only the seconds and nanoseconds a client carries count.
func nfsTimeMatches(set *nfs4.NfsTimeSet, info nfsfs.Info) bool {
if set.Server {
return false
}
got := info.ModTime
return got.Unix() == set.Time.Seconds && int64(uint32(got.Nanosecond())) == int64(set.Time.Nseconds)
}
// errTooManyStateids marks a TEST_STATEID whose array exceeds the bound
// every COMPOUND array of this build obeys.
var errTooManyStateids = errors.New("nfs4server: too many stateids in one test")
// releaseLockOwnerOp serves RELEASE_LOCKOWNER: every lock state the
// owner holds dies with the owner. The owner must belong to the asking
// client, RFC 8881 section 18.38.
func (h *Handler) releaseLockOwnerOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) {
clientid, err := d.Uint64()
if err != nil {
return nil, 0, err
}
owner, err := d.VarOpaque()
if err != nil {
return nil, 0, err
}
if sessionClientid != 0 && clientid != sessionClientid {
return nil, nfs4.ErrBadOwner, nil
}
h.locks().releaseOwner(clientid, owner)
return nil, nfs4.ErrOK, nil
}
// delegReturnOp serves DELEGRETURN: the client gives its own delegation
// back. A stateid the store holds is dropped; anything else is answered
// OK just the same, because the release of a state the server does not
// carry is already done and the reply only tells the client to forget
// the stateid.
func (h *Handler) delegReturnOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) {
var st nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(st[:], raw)
h.delegs().dropStateid(st, sessionClientid)
return nil, nfs4.ErrOK, nil
}
// backchannelCtlOp serves BACKCHANNEL_CTL: the client names the program
// its future back channels run.
func (h *Handler) backchannelCtlOp(d *xdr.Decoder, ctx *connCB) ([]byte, uint32, error) {
program, err := d.Uint32()
if err != nil {
return nil, 0, err
}
if ctx != nil {
ctx.setProgram(program)
}
return nil, nfs4.ErrOK, nil
}
// bindConnToSessionOp serves BIND_CONN_TO_SESSION: the connection the
// request rode in on joins the session as a fore channel, a back channel
// or both. The session must belong to the asking client, so one client
// cannot graft its connection onto another's callbacks, and the
// callback program comes from CREATE_SESSION, which stores it with the
// session.
func (h *Handler) bindConnToSessionOp(d *xdr.Decoder, ctx *connCB, sessionClientid uint64) ([]byte, uint32, error) {
var id nfs4.SessionID
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(id[:], raw)
dir, err := d.Uint32()
if err != nil {
return nil, 0, err
}
if _, err = d.Bool(); err != nil { // use conn in RDMA mode
return nil, 0, err
}
if _, status := h.sessions().lookupSession(id); status != nfs4.ErrOK {
return nil, status, nil
}
// A compound that carries a session may bind only its own: one
// client must not graft its connection onto another's callbacks.
if sessionClientid != 0 && id.ClientIDOf() != sessionClientid {
return nil, nfs4.ErrBadSession, nil
}
switch dir {
case nfs4.Cdfc4Back, nfs4.Cdfc4BackOrBoth:
if ctx == nil {
return nil, nfs4.ErrConnNotBound, nil
}
h.sessions().attachCB(id, ctx)
return nfs4.AppendBindConnToSessionRes(nil, id, nfs4.Cdfs4Both), nfs4.ErrOK, nil
case nfs4.Cdfc4Fore, nfs4.Cdfc4ForeOrBoth:
return nfs4.AppendBindConnToSessionRes(nil, id, nfs4.Cdfs4Fore), nfs4.ErrOK, nil
default:
return nil, nfs4.ErrInval, nil
}
}
// freeStateidOp serves FREE_STATEID: the client retires a lock stateid
// of its own whose owner holds no locks any more.
func (h *Handler) freeStateidOp(d *xdr.Decoder, sessionClientid uint64) ([]byte, uint32, error) {
var st nfs4.Stateid
raw, err := d.Raw(16)
if err != nil {
return nil, 0, err
}
copy(st[:], raw)
if status := h.locks().freeStateid(st, sessionClientid); status != nfs4.ErrOK {
return nil, status, nil
}
return nil, nfs4.ErrOK, nil
}
// testStateidOp serves TEST_STATEID: one status per stateid, no state
// touched.
func (h *Handler) testStateidOp(d *xdr.Decoder) ([]byte, uint32, error) {
n, err := d.Uint32()
if err != nil {
return nil, 0, err
}
if n > maxOps {
return nil, 0, errTooManyStateids
}
var statuses []uint32
for range n {
var st nfs4.Stateid
raw, rerr := d.Raw(16)
if rerr != nil {
return nil, 0, rerr
}
copy(st[:], raw)
status := h.openStates().testStateid(st)
if status == nfs4.ErrBadStateid {
switch {
case h.locks().hasStateid(st):
status = nfs4.ErrOK
case h.delegs().hasStateid(st):
status = nfs4.ErrOK
}
}
statuses = append(statuses, status)
}
return nfs4.AppendTestStateidRes(nil, statuses), nfs4.ErrOK, nil
}