Files
nfs/internal/nfs4server/kerberos_test.go
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

160 lines
4.4 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4server
import (
"net"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/rpc"
"sourcedock.dev/petrbalvin/nfs/internal/server"
"sourcedock.dev/petrbalvin/nfs/internal/krb5"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfsclient"
)
// The three RPCSEC_GSS service levels round trip against the real TCP
// client: the credential sequence window, the verifier MIC over the
// call header, the checksummed arguments and results at integrity and
// the sealed ones at privacy.
func TestKerberosServiceLevels(t *testing.T) {
h := testTree(t)
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 1)
}
h.ServerKey = key
h.ServiceName = "nfs"
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
srv := &server.Server{Handle: h.HandleConn}
go srv.Serve(t.Context(), ln)
defer ln.Close()
cases := []struct {
name string
svc uint32
}{
{"krb5", rpc.SvcNone},
{"krb5i", rpc.SvcIntegrity},
{"krb5p", rpc.SvcPrivacy},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cl, err := nfsclient.Dial(ln.Addr().String())
if err != nil {
t.Fatal(err)
}
defer cl.Close()
// The session exists before the GSS switch: the COMPOUND
// then carries SEQUENCE under the GSS credential.
if err := cl.Establish("gss-" + tc.name); err != nil {
t.Fatalf("establish: %v", err)
}
if err := cl.EnableGSS(krb5.EtypeAES256, key, "EXAMPLE.ORG", "nfs",
"petr@EXAMPLE.ORG", tc.svc); err != nil {
t.Fatalf("enable gss: %v", err)
}
res, bodies, err := cl.Compound("gss", [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType, nfs4.AttrSize)),
})
if err != nil {
t.Fatalf("compound: %v", err)
}
if res.Status != nfs4.ErrOK || len(bodies) != 2 {
t.Fatalf("compound: status %d bodies %d", res.Status, len(bodies))
}
// A second call walks the sequence window one further.
res, _, err = cl.Compound("gss2", [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendLookup(nil, "a.txt"),
})
if err != nil || res.Status != nfs4.ErrOK {
t.Fatalf("second compound: status %d %v", res.Status, err)
}
if err := cl.DisableGSS(); err != nil {
t.Fatalf("disable: %v", err)
}
// After the destroy the client falls back to AUTH_SYS and the
// compound succeeds anonymously again.
res2, _, err2 := cl.Compound("after", [][]byte{nfs4.AppendPutRootfh(nil)})
if err2 != nil || res2.Status != nfs4.ErrOK {
t.Fatalf("compound after disable: %d %v", res2.Status, err2)
}
})
}
}
// RPCSEC_GSSv3: the CREATE control procedure binds assertions to a
// child handle and the compounds under the child carry the version
// three credential, RFC 7861.
func TestGSSv3CreateAndUse(t *testing.T) {
h := testTree(t)
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 9)
}
h.ServerKey = key
h.ServiceName = "nfs"
addr := startCBServer(t, h)
cl, err := nfsclient.Dial(addr)
if err != nil {
t.Fatal(err)
}
defer cl.Close()
if err := cl.Establish("v3"); err != nil {
t.Fatalf("establish: %v", err)
}
if err := cl.EnableGSS(krb5.EtypeAES256, key, "EXAMPLE.ORG", "nfs",
"petr@EXAMPLE.ORG", rpc.SvcIntegrity); err != nil {
t.Fatalf("enable gss: %v", err)
}
// CREATE with a label assertion over the parent context.
child, err := cl.CreateGSSChild([]rpc.Assertion{{
Type: rpc.AssertionLabel,
Label: rpc.Label{
LfsId: 1,
PiId: 0,
Bytes: []byte("secret"),
},
}})
if err != nil {
t.Fatalf("create: %v", err)
}
if len(child) == 0 {
t.Fatal("no child handle")
}
// A compound under the child handle rides the version three
// credential at the integrity level.
res, _, err := cl.Compound("v3", [][]byte{
nfs4.AppendPutRootfh(nil),
nfs4.AppendLookup(nil, "a.txt"),
})
if err != nil || res.Status != nfs4.ErrOK {
t.Fatalf("compound under child: status %d %v", res.Status, err)
}
// The server bound the label to the child context.
if lbl := h.labelOf(child); lbl == nil || string(lbl.Bytes) != "secret" {
t.Fatalf("label not bound: %+v", lbl)
}
// LIST answers the supported assertion types.
types, err := cl.ListGSSAssertions()
if err != nil {
t.Fatalf("list: %v", err)
}
if len(types) != 2 {
t.Fatalf("list types %v", types)
}
}