// Copyright (c) 2026 Petr BalvĂ­n (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")