Files
nfs/internal/nfs4/session.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

424 lines
13 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The session establishment layer of RFC 8881 sections 18.35 to 18.37 and
// 18.46: EXCHANGE_ID names the client, CREATE_SESSION makes the session
// with its slot table, SEQUENCE drives the slots, DESTROY_SESSION tears
// one down.
package nfs4
import (
"encoding/binary"
"errors"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// EXCHGID4_FLAG values, RFC 8881 section 18.35.
const (
ExchgIDSuppMovedRefer = 0x00000001
ExchgIDSuppMovedMigr = 0x00000002
ExchgIDSuppFenceOps = 0x00000004
ExchgIDBindPrincStateid = 0x00000100
ExchgIDUseNonPnfs = 0x00010000
ExchgIDUsePnfsMds = 0x00020000
ExchgIDUsePnfsDs = 0x00040000
ExchgIDConfirmedR = 0x80000000
)
// CREATE_SESSION4_FLAG values.
const (
CreateSessionFlagPersist = 0x00000001
CreateSessionFlagConnBackChan = 0x00000002
CreateSessionFlagConnRdma = 0x00000004
)
// state protection choices of the EXCHANGE_ID unions, the
// state_protect_how4 enumeration of RFC 8881 section 18.35. Only the
// plain SP4_NONE is spoken by this build.
const (
StateProtectNone = 0 // SP4_NONE
StateProtectMachCred = 1 // SP4_MACH_CRED
StateProtectSSV = 2 // SP4_SSV
)
// A SessionID is the 16 byte session identifier RFC 7863 fixes
// (NFS4_SESSIONID_SIZE): an 8 byte server prefix followed by the 8 byte
// client id, both chosen by the server.
type SessionID [16]byte
// ClientIDOf splits the session id into its client id half.
func (s SessionID) ClientIDOf() uint64 {
return binary.BigEndian.Uint64(s[8:16])
}
// MakeSessionID builds a session id from the server prefix and the client
// id.
func MakeSessionID(prefix [8]byte, clientid uint64) SessionID {
var s SessionID
copy(s[:8], prefix[:])
binary.BigEndian.PutUint64(s[8:], clientid)
return s
}
// MakeNumberedSessionID builds a session id whose server half carries a
// per-session number, so every CREATE_SESSION mints a distinct id as
// RFC 8881 section 18.36 requires: four prefix bytes, the number and
// the client id.
func MakeNumberedSessionID(prefix [4]byte, number uint32, clientid uint64) SessionID {
var s SessionID
copy(s[:4], prefix[:])
binary.BigEndian.PutUint32(s[4:], number)
binary.BigEndian.PutUint64(s[8:], clientid)
return s
}
// A ChannelAttrs is the channel_attrs4 of a session: the sizes and limits
// of one connection direction.
type ChannelAttrs struct {
HeaderPad uint32
MaxRequest uint32
MaxResponse uint32
MaxRespResourced uint32
MaxOperations uint32
MaxRequests uint32
RdmaIRDEnabled bool
RdmaIRSizes []uint32
}
// DefaultForeChannel is the fore channel this server grants: enough slots
// for a real client, bounded to what one connection streams.
var DefaultForeChannel = ChannelAttrs{
MaxRequest: 1 << 22,
MaxResponse: 1 << 22,
MaxRespResourced: 1 << 22,
MaxOperations: 16,
MaxRequests: 8,
}
// DefaultBackChannel declares the minimal back channel: one operation and
// one request, with no channel attributes, which the client reads as
// nothing offered.
var DefaultBackChannel = ChannelAttrs{MaxRequest: 1 << 20, MaxResponse: 1 << 20, MaxRespResourced: 1 << 20, MaxOperations: 1, MaxRequests: 1}
// ErrOpMisordered marks a SEQUENCE that arrived on a slot with a sequence
// that is neither a retry nor the next one.
var ErrOpMisordered = errors.New("nfs4: sequence misordered")
// ExchangeIDArgs is the decoded EXCHANGE_ID4args.
type ExchangeIDArgs struct {
Verifier [8]byte
OwnerID []byte
Flags uint32
Protect uint32
}
// AppendExchangeIDArgs encodes the EXCHANGE_ID4args. The implementation id
// array is sent empty.
func AppendExchangeIDArgs(b []byte, verifier [8]byte, ownerID []byte, flags uint32) []byte {
b = xdr.AppendUint32(b, OpExchangeID)
b = append(b, verifier[:]...)
b = xdr.AppendVarOpaque(b, ownerID)
b = xdr.AppendUint32(b, flags)
b = xdr.AppendUint32(b, StateProtectNone)
return xdr.AppendUint32(b, 0) // empty eia_client_impl_id
}
// DecodeExchangeIDArgs decodes the EXCHANGE_ID4args from the decoder,
// which is positioned after the operation number.
func DecodeExchangeIDArgs(d *xdr.Decoder) (ExchangeIDArgs, error) {
var a ExchangeIDArgs
verf, err := d.Raw(8)
if err != nil {
return a, err
}
copy(a.Verifier[:], verf)
if a.OwnerID, err = d.VarOpaque(); err != nil {
return a, err
}
if a.Flags, err = d.Uint32(); err != nil {
return a, err
}
if a.Protect, err = d.Uint32(); err != nil {
return a, err
}
if a.Protect != StateProtectNone {
return a, ErrStateProtectNotSupp
}
// The client implementation id array is walked and ignored.
n, err := d.Uint32()
if err != nil {
return a, err
}
for range n {
if _, err = d.String(); err != nil {
return a, err
}
if _, err = d.String(); err != nil {
return a, err
}
if _, err = d.Uint64(); err != nil {
return a, err
}
if _, err = d.Uint32(); err != nil {
return a, err
}
}
return a, nil
}
// AppendExchangeIDRes encodes a successful EXCHANGE_ID4res: the client id,
// the create session sequence, the flags, the plain state protection, the
// server owner, the server scope and one implementation id entry, RFC 8881
// section 18.35. The field order and the entry shape follow what real
// clients decode: the minor id is a uint64 and the major id a bounded
// opaque, the scope follows the owner, and the entries carry no dummy.
func AppendExchangeIDRes(b []byte, clientid uint64, sequence, flags uint32, majorID []byte) []byte {
b = xdr.AppendUint64(b, clientid)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, flags)
b = xdr.AppendUint32(b, StateProtectNone)
// eir_server_owner: the major id stays constant across restarts so
// the client recognises the server.
b = xdr.AppendUint64(b, 0)
b = xdr.AppendVarOpaque(b, majorID)
// eir_server_scope: the servers that share this one's state. The
// Linux client fails the exchange when the field is missing.
b = xdr.AppendVarOpaque(b, majorID)
// eir_server_impl_id: one entry of domain, name and the zeroed date.
b = xdr.AppendUint32(b, 1)
b = xdr.AppendString(b, "sourcedock.dev")
b = xdr.AppendString(b, "nfsd")
b = xdr.AppendInt64(b, 0)
b = xdr.AppendUint32(b, 0)
return b
}
// AppendSessionChannel encodes a channel_attrs4.
func AppendSessionChannel(b []byte, c ChannelAttrs) []byte {
b = xdr.AppendUint32(b, c.HeaderPad)
b = xdr.AppendUint32(b, c.MaxRequest)
b = xdr.AppendUint32(b, c.MaxResponse)
b = xdr.AppendUint32(b, c.MaxRespResourced)
b = xdr.AppendUint32(b, c.MaxOperations)
b = xdr.AppendUint32(b, c.MaxRequests)
if c.RdmaIRDEnabled {
b = xdr.AppendUint32(b, uint32(len(c.RdmaIRSizes)))
for _, v := range c.RdmaIRSizes {
b = xdr.AppendUint32(b, v)
}
return b
}
// ca_rdma_ird is a counted array, RFC 8881 section 18.36: an empty
// one counts zero. Every real client decoder rejects anything else;
// the Linux client answers EINVAL for a count above one.
return xdr.AppendUint32(b, 0)
}
// DecodeSessionChannel decodes a channel_attrs4 body.
func DecodeSessionChannel(d *xdr.Decoder) (ChannelAttrs, error) {
var c ChannelAttrs
var err error
if c.HeaderPad, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxRequest, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxResponse, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxRespResourced, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxOperations, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxRequests, err = d.Uint32(); err != nil {
return c, err
}
count, err := d.Uint32()
if err != nil {
return c, err
}
// ca_rdma_ird is a counted array: zero means no RDMA IRD support.
if count > 0 {
c.RdmaIRDEnabled = true
for range count {
v, verr := d.Uint32()
if verr != nil {
return c, verr
}
c.RdmaIRSizes = append(c.RdmaIRSizes, v)
}
}
return c, nil
}
// AppendCreateSessionArgs encodes the CREATE_SESSION4args.
func AppendCreateSessionArgs(b []byte, clientid uint64, sequence, flags uint32, fore, back ChannelAttrs, cbProgram uint32) []byte {
b = xdr.AppendUint32(b, OpCreateSession)
b = xdr.AppendUint64(b, clientid)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, flags)
b = AppendSessionChannel(b, fore)
b = AppendSessionChannel(b, back)
return xdr.AppendUint32(b, cbProgram)
}
// A CreateSessionArgs is the decoded CREATE_SESSION4args.
type CreateSessionArgs struct {
ClientID uint64
Sequence uint32
Flags uint32
Fore ChannelAttrs
Back ChannelAttrs
CBProgram uint32
}
// DecodeCreateSessionArgs decodes the CREATE_SESSION4args from the
// decoder, which is positioned after the operation number.
func DecodeCreateSessionArgs(d *xdr.Decoder) (CreateSessionArgs, error) {
var a CreateSessionArgs
var err error
if a.ClientID, err = d.Uint64(); err != nil {
return a, err
}
if a.Sequence, err = d.Uint32(); err != nil {
return a, err
}
if a.Flags, err = d.Uint32(); err != nil {
return a, err
}
if a.Fore, err = DecodeSessionChannel(d); err != nil {
return a, err
}
if a.Back, err = DecodeSessionChannel(d); err != nil {
return a, err
}
if a.CBProgram, err = d.Uint32(); err != nil {
return a, err
}
return a, nil
}
// AppendCreateSessionRes encodes a successful CREATE_SESSION4res.
func AppendCreateSessionRes(b []byte, id SessionID, sequence, flags uint32, fore, back ChannelAttrs) []byte {
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, flags)
b = AppendSessionChannel(b, fore)
return AppendSessionChannel(b, back)
}
// AppendDestroySessionArgs appends the DESTROY_SESSION argop.
func AppendDestroySessionArgs(b []byte, id SessionID) []byte {
b = xdr.AppendUint32(b, OpDestroySession)
return append(b, id[:]...)
}
// AppendSequenceArgs appends the SEQUENCE argop.
func AppendSequenceArgs(b []byte, id SessionID, sequence, slot, highestSlot uint32, cacheThis bool) []byte {
b = xdr.AppendUint32(b, OpSequence)
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, slot)
b = xdr.AppendUint32(b, highestSlot)
return xdr.AppendBool(b, cacheThis)
}
// A SequenceArgs is the decoded SEQUENCE4args.
type SequenceArgs struct {
SessionID SessionID
Sequence uint32
Slot uint32
HighestSlot uint32
CacheThis bool
}
// DecodeSequenceArgs decodes the SEQUENCE4args from the decoder, which
// is positioned after the operation number.
func DecodeSequenceArgs(d *xdr.Decoder) (SequenceArgs, error) {
var a SequenceArgs
raw, err := d.Raw(16)
if err != nil {
return a, err
}
copy(a.SessionID[:], raw)
if a.Sequence, err = d.Uint32(); err != nil {
return a, err
}
if a.Slot, err = d.Uint32(); err != nil {
return a, err
}
if a.HighestSlot, err = d.Uint32(); err != nil {
return a, err
}
if a.CacheThis, err = d.Bool(); err != nil {
return a, err
}
return a, nil
}
// NegotiateChannel clamps the requested channel attributes to the
// server's limits, RFC 8881 section 18.36: every negotiated value is the
// smaller of the request and the limit. A zero request carries meaning,
// ca_maxresponsesize_cached of zero above all, so it is answered with
// zero. Real clients reject a reply that exceeds their request.
func NegotiateChannel(requested ChannelAttrs, limits ChannelAttrs) ChannelAttrs {
nv := func(requested, limit uint32) uint32 {
if requested > limit {
return limit
}
return requested
}
return ChannelAttrs{
MaxRequest: nv(requested.MaxRequest, limits.MaxRequest),
MaxResponse: nv(requested.MaxResponse, limits.MaxResponse),
MaxRespResourced: nv(requested.MaxRespResourced, limits.MaxRespResourced),
MaxOperations: nv(requested.MaxOperations, limits.MaxOperations),
MaxRequests: nv(requested.MaxRequests, limits.MaxRequests),
}
}
// AppendSequenceRes encodes the successful SEQUENCE4resok of RFC 7863:
// the session id, the echoed identifiers and the status flags. The
// target highest slot mirrors the highest slot this build grants; a
// caller with its own view passes it instead.
func AppendSequenceRes(b []byte, id SessionID, sequence, slot, highestSlot, statusFlags uint32) []byte {
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, slot)
b = xdr.AppendUint32(b, highestSlot)
b = xdr.AppendUint32(b, highestSlot) // sr_target_highest_slotid
return xdr.AppendUint32(b, statusFlags)
}
// DecodeSequenceRes decodes the successful SEQUENCE4resok.
func DecodeSequenceRes(d *xdr.Decoder) (id SessionID, sequence, slot, highestSlot, targetHighestSlot, statusFlags uint32, err error) {
raw, err := d.Raw(16)
if err != nil {
return
}
copy(id[:], raw)
if sequence, err = d.Uint32(); err != nil {
return
}
if slot, err = d.Uint32(); err != nil {
return
}
if highestSlot, err = d.Uint32(); err != nil {
return
}
if targetHighestSlot, err = d.Uint32(); err != nil {
return
}
statusFlags, err = d.Uint32()
return
}
// ErrStateProtectNotSupp marks an EXCHANGE_ID that named a state
// protection this build does not speak.
var ErrStateProtectNotSupp = errors.New("nfs4: state protection not supported")