424 lines
13 KiB
Go
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")
|