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

318 lines
11 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestSessionIDShape(t *testing.T) {
// The session id is sixteen bytes on the wire, RFC 7863: an eight
// byte server prefix and the eight byte client id.
var prefix [8]byte
copy(prefix[:], []byte("nfs-srv-"))
id := MakeSessionID(prefix, 0xdeadbeefcafe)
if len(id) != 16 {
t.Fatalf("session id of %d bytes, want 16", len(id))
}
if id.ClientIDOf() != 0xdeadbeefcafe {
t.Fatalf("client id half: %x", id.ClientIDOf())
}
if !bytes.Equal(id[:8], prefix[:]) {
t.Fatalf("server prefix: %x", id[:8])
}
}
func TestExchangeIDRoundTrip(t *testing.T) {
var verifier [8]byte
copy(verifier[:], []byte("verif-1-"))
arg := AppendExchangeIDArgs(nil, verifier, []byte("owner-1"), ExchgIDUseNonPnfs)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpExchangeID {
t.Fatalf("op: %d, %v", op, err)
}
a, err := DecodeExchangeIDArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if a.Verifier != verifier || string(a.OwnerID) != "owner-1" ||
a.Flags != ExchgIDUseNonPnfs || a.Protect != StateProtectNone {
t.Fatalf("args: %+v", a)
}
res := AppendExchangeIDRes(nil, 77, 0, ExchgIDUseNonPnfs|ExchgIDConfirmedR, []byte("major"))
rd := xdr.NewDecoder(res)
if clientid, err := rd.Uint64(); err != nil || clientid != 77 {
t.Fatalf("clientid: %d, %v", clientid, err)
}
if seq, err := rd.Uint32(); err != nil || seq != 0 {
t.Fatalf("sequence: %d, %v", seq, err)
}
if flags, err := rd.Uint32(); err != nil || flags&ExchgIDConfirmedR == 0 {
t.Fatalf("flags: %x, %v", flags, err)
}
if n, err := resBodyLen(OpExchangeID, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
}
}
func TestCreateSessionRoundTrip(t *testing.T) {
arg := AppendCreateSessionArgs(nil, 0x1234, 3, CreateSessionFlagPersist,
DefaultForeChannel, DefaultBackChannel, 0xcafe)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpCreateSession {
t.Fatalf("op: %d, %v", op, err)
}
a, err := DecodeCreateSessionArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if a.ClientID != 0x1234 || a.Sequence != 3 || a.Flags != CreateSessionFlagPersist ||
a.Fore.MaxRequest != DefaultForeChannel.MaxRequest ||
a.Fore.MaxRequests != DefaultForeChannel.MaxRequests ||
a.Back.MaxRequest != DefaultBackChannel.MaxRequest ||
a.CBProgram != 0xcafe {
t.Fatalf("args: %+v", a)
}
id := MakeSessionID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 0x1234)
res := AppendCreateSessionRes(nil, id, 3, 0, DefaultForeChannel, DefaultBackChannel)
if n, err := resBodyLen(OpCreateSession, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
}
}
func TestSequenceAndDestroyWire(t *testing.T) {
id := MakeSessionID([8]byte{9, 9, 9, 9, 9, 9, 9, 9}, 42)
arg := AppendSequenceArgs(nil, id, 5, 1, 7, true)
a, err := DecodeSequenceArgs(xdr.NewDecoder(arg[4:]))
if err != nil {
t.Fatalf("decode: %v", err)
}
if a.SessionID != id || a.Sequence != 5 || a.Slot != 1 ||
a.HighestSlot != 7 || !a.CacheThis {
t.Fatalf("args: %+v", a)
}
// The result carries exactly the five words after the session id
// that RFC 7863 fixes: sequence, slot, highest, target, flags.
res := AppendSequenceRes(nil, id, 5, 1, 7, 0)
if len(res) != 36 {
t.Fatalf("sequence result of %d bytes, want 36", len(res))
}
if n, err := resBodyLen(OpSequence, ErrOK, res); err != nil || n != 36 {
t.Fatalf("sequence measure: %d, %v", n, err)
}
flags, err := xdr.NewDecoder(res[32:]).Uint32()
if err != nil || flags != 0 {
t.Fatalf("sr_status_flags: %d, %v", flags, err)
}
sid, seq, slot, high, target, statusFlags, err := DecodeSequenceRes(xdr.NewDecoder(res))
if err != nil || sid != id || seq != 5 || slot != 1 || high != 7 ||
target != 7 || statusFlags != 0 {
t.Fatalf("decode res: %d %d %d %d %d %d, %v", seq, slot, high, target, statusFlags, 0, err)
}
destroy := AppendDestroySessionArgs(nil, id)
d := xdr.NewDecoder(destroy)
if op, err := d.Uint32(); err != nil || op != OpDestroySession {
t.Fatalf("destroy op: %d, %v", op, err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, id[:]) {
t.Fatalf("destroy id: %x, %v", raw, err)
}
}
func TestExchangeIDImplArray(t *testing.T) {
// One implementation entry walks exactly its three fields: domain,
// name and the date, the shape RFC 7863 fixes. A trailing dummy
// word stays in the stream and is the next operation's business.
var blob []byte
blob = append(blob, make([]byte, 8)...) // verifier
blob = xdr.AppendVarOpaque(blob, []byte("id")) // owner
blob = xdr.AppendUint32(blob, 0) // flags
blob = xdr.AppendUint32(blob, StateProtectNone)
blob = xdr.AppendUint32(blob, 1) // one impl entry
blob = xdr.AppendString(blob, "example.org")
blob = xdr.AppendString(blob, "probe")
blob = xdr.AppendUint64(blob, 0) // date seconds
blob = xdr.AppendUint32(blob, 0) // date nanoseconds
d := xdr.NewDecoder(blob)
a, err := DecodeExchangeIDArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if string(a.OwnerID) != "id" || d.Remaining() != 0 {
t.Fatalf("owner %q, %d bytes left", a.OwnerID, d.Remaining())
}
// The word some clients append behind the entry is not consumed by
// this decoder; the compound walk sees it as the next argop.
withExtra := append(append([]byte{}, blob...), 0, 0, 0, 0)
d = xdr.NewDecoder(withExtra)
if _, err := DecodeExchangeIDArgs(d); err != nil || d.Remaining() != 4 {
t.Fatalf("decode with extra: %v, %d bytes left", err, d.Remaining())
}
}
func TestDecodeSessionChannelIrd(t *testing.T) {
// A channel with the RDMA sizes present walks them all.
var body []byte
body = xdr.AppendUint32(body, 0) // header pad
body = xdr.AppendUint32(body, 1<<20) // max request
body = xdr.AppendUint32(body, 1<<20) // max response
body = xdr.AppendUint32(body, 1<<20) // resourced
body = xdr.AppendUint32(body, 4) // max operations
body = xdr.AppendUint32(body, 2) // max requests
body = xdr.AppendUint32(body, 2) // two rdma ird sizes
body = xdr.AppendUint32(body, 1)
body = xdr.AppendUint32(body, 4)
c, err := DecodeSessionChannel(xdr.NewDecoder(body))
if err != nil {
t.Fatalf("decode: %v", err)
}
if !c.RdmaIRDEnabled || len(c.RdmaIRSizes) != 2 || c.RdmaIRSizes[1] != 4 ||
c.MaxRequests != 2 {
t.Fatalf("channel: %+v", c)
}
}
func TestExchangeIDStateProtectReject(t *testing.T) {
// A state protection other than the plain none is refused with a
// dedicated error, because this build does not speak RPCSEC_GSS
// state protection yet.
var blob []byte
blob = append(blob, make([]byte, 8)...)
blob = xdr.AppendVarOpaque(blob, []byte("id"))
blob = xdr.AppendUint32(blob, 0)
blob = xdr.AppendUint32(blob, StateProtectMachCred)
if _, err := DecodeExchangeIDArgs(xdr.NewDecoder(blob)); err == nil {
t.Fatal("an unsupported state protection decoded")
}
}
func TestOpenCloseWire(t *testing.T) {
arg := AppendOpenArgs(nil, 0xfeed, []byte("owner"), ShareAccessBoth, uint32(0),
true, 0o640, "file.txt")
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpen {
t.Fatalf("op: %d, %v", op, err)
}
a, err := DecodeOpenArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if !a.Create || a.Access != ShareAccessBoth || a.Deny != uint32(0) ||
a.ClientID != 0xfeed || string(a.Owner) != "owner" || a.Perm != 0o640 ||
a.Name != "file.txt" {
t.Fatalf("open args: %+v", a)
}
// NOCREATE: the create switch is absent.
arg = AppendOpenArgs(nil, 1, []byte("o"), ShareAccessRead, 0, false, 0, "there.txt")
a, err = DecodeOpenArgs(xdr.NewDecoder(arg[4:]))
if err != nil || a.Create || a.Name != "there.txt" {
t.Fatalf("nocreate args: %+v, %v", a, err)
}
res := AppendOpenRes(nil, AllZero)
if n, err := resBodyLen(OpOpen, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("open measure: %d of %d, %v", n, len(res), err)
}
closeArg := AppendCloseArgs(nil, AllZero)
d = xdr.NewDecoder(closeArg)
if op, err := d.Uint32(); err != nil || op != OpClose {
t.Fatalf("close op: %d, %v", op, err)
}
if seqid, err := d.Uint32(); err != nil || seqid != 0 {
t.Fatalf("close seqid: %d, %v", seqid, err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, AllZero[:]) {
t.Fatalf("close stateid: %x, %v", raw, err)
}
closeRes := AppendCloseRes(nil, AllZero)
if n, err := resBodyLen(OpClose, ErrOK, closeRes); err != nil || n != 16 {
t.Fatalf("close measure: %d, %v", n, err)
}
}
func TestOpenDowngradeDestroyReclaimWire(t *testing.T) {
// OPEN_DOWNGRADE args: seqid, stateid, access, deny.
arg := AppendOpenDowngradeArgs(nil, AllZero, ShareAccessRead, 0)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpenDowngrade {
t.Fatalf("op: %d, %v", op, err)
}
if seqid, err := d.Uint32(); err != nil || seqid != 0 {
t.Fatalf("seqid: %d, %v", seqid, err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, AllZero[:]) {
t.Fatalf("stateid: %x, %v", raw, err)
}
if access, err := d.Uint32(); err != nil || access != ShareAccessRead {
t.Fatalf("access: %v", err)
}
if deny, err := d.Uint32(); err != nil || deny != 0 {
t.Fatalf("deny: %v", err)
}
dgRes := AppendOpenDowngradeRes(nil)
if n, err := resBodyLen(OpOpenDowngrade, ErrOK, dgRes); err != nil || n != 20 {
t.Fatalf("downgrade measure: %d, %v", n, err)
}
// DESTROY_CLIENTID args and its empty result body.
dc := AppendDestroyClientIDArgs(nil, 0x1234)
d = xdr.NewDecoder(dc)
if op, err := d.Uint32(); err != nil || op != OpDestroyClientID {
t.Fatalf("destroy op: %d, %v", op, err)
}
if cid, err := d.Uint64(); err != nil || cid != 0x1234 {
t.Fatalf("clientid: %v", err)
}
if n, err := resBodyLen(OpDestroyClientID, ErrOK, nil); err != nil || n != 0 {
t.Fatalf("destroy measure: %d, %v", n, err)
}
// RECLAIM_COMPLETE args: the one client flag alone, RFC 7863.
rc := AppendReclaimCompleteArgs(nil, false)
d = xdr.NewDecoder(rc)
if op, err := d.Uint32(); err != nil || op != OpReclaimComplete {
t.Fatalf("op: %d, %v", op, err)
}
if one, err := d.Bool(); err != nil || one {
t.Fatalf("one_client: %v", err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the reclaim args", d.Remaining())
}
if n, err := resBodyLen(OpReclaimComplete, ErrOK, nil); err != nil || n != 0 {
t.Fatalf("reclaim measure: %d, %v", n, err)
}
}
func TestAppendOpenResDelegLength(t *testing.T) {
// The write delegation carries the space limit union and the
// permissions ace on top of the read form, RFC 7863: the whole
// successful OPEN result measures 96 bytes with one.
var st Stateid
st[0] = 1
res := AppendOpenResDeleg(nil, st, OpenDelegWrite, st)
if len(res) != 96 {
t.Fatalf("res %d bytes, want 96", len(res))
}
// The read form ends after the ace: 84 bytes.
res = AppendOpenResDeleg(nil, st, OpenDelegRead, st)
if len(res) != 84 {
t.Fatalf("read res %d bytes, want 84", len(res))
}
if _, _, _, err := DecodeOpenResDeleg(res); err != nil {
t.Fatalf("read round trip: %v", err)
}
}