feat: full NFSv4.2 server and client in pure Go
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
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
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user