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,149 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nfsclient
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/krb5"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfs4server"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/rpc"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/server"
|
||||
)
|
||||
|
||||
func testCert(t *testing.T) *tls.Certificate {
|
||||
t.Helper()
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{CommonName: "nfs.test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv}
|
||||
}
|
||||
|
||||
// The whole modern security stack of one client session: the AUTH_TLS
|
||||
// upgrade of RFC 9289, the RPCSEC_GSS context of RFC 2203 and the
|
||||
// version three child context with assertions of RFC 7861, all against
|
||||
// the real server over TCP at the privacy level.
|
||||
func TestTLSAndGSSv3Stack(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "s.txt"), []byte("stack"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend, err := nfsfs.NewLocal(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cert := testCert(t)
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = byte(i + 5)
|
||||
}
|
||||
h := &nfs4server.Handler{FS: backend, ServerKey: key, ServiceName: "nfs",
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{*cert}}}
|
||||
|
||||
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()
|
||||
|
||||
cl, err := Dial(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cl.Close()
|
||||
|
||||
// The TLS upgrade rides the AUTH_TLS probe.
|
||||
cfg := &tls.Config{ServerName: "localhost",
|
||||
RootCAs: func() *x509.CertPool {
|
||||
pool := x509.NewCertPool()
|
||||
leaf, lerr := x509.ParseCertificate(cert.Certificate[0])
|
||||
if lerr != nil {
|
||||
t.Fatal(lerr)
|
||||
}
|
||||
pool.AddCert(leaf)
|
||||
return pool
|
||||
}()}
|
||||
if err := cl.EnableTLS(cfg); err != nil {
|
||||
t.Fatalf("enable tls: %v", err)
|
||||
}
|
||||
|
||||
// The session and the parent GSS context establish on the encrypted
|
||||
// channel.
|
||||
if err := cl.Establish("stack"); err != nil {
|
||||
t.Fatalf("establish: %v", err)
|
||||
}
|
||||
if err := cl.EnableGSS(krb5.EtypeAES256, key, "EXAMPLE.ORG", "nfs",
|
||||
"petr@EXAMPLE.ORG", rpc.SvcPrivacy); err != nil {
|
||||
t.Fatalf("enable gss: %v", err)
|
||||
}
|
||||
|
||||
// CREATE binds a label assertion to a version three child handle.
|
||||
child, err := cl.CreateGSSChild([]rpc.Assertion{{
|
||||
Type: rpc.AssertionLabel,
|
||||
Label: rpc.Label{LfsId: 4, Bytes: []byte("top")},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
// A compound under the child handle rides TLS plus the privacy
|
||||
// protected version three credential.
|
||||
res, _, err := cl.Compound("stack", [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "s.txt"),
|
||||
})
|
||||
if err != nil || res.Status != nfs4.ErrOK {
|
||||
t.Fatalf("compound: 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) != "top" {
|
||||
t.Fatalf("label not bound")
|
||||
}
|
||||
|
||||
// LIST answers the supported assertion types.
|
||||
types, err := cl.ListGSSAssertions()
|
||||
if err != nil {
|
||||
t.Fatalf("list: %v", err)
|
||||
}
|
||||
if len(types) != 2 {
|
||||
t.Fatalf("types %v", types)
|
||||
}
|
||||
|
||||
// The destroy of the parent retires the child with it.
|
||||
if err := cl.DisableGSS(); err != nil {
|
||||
t.Fatalf("disable: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user