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
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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user