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,131 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Command nfsd serves NFS over TCP. It is the server binary of the nfs
|
||||
// project: it exports one local directory tree over NFSv4.2 as described
|
||||
// in docs/ARCHITECTURE.md.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfs4server"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flags := flag.CommandLine
|
||||
addr := flags.String("addr", ":2049", "TCP address to listen on")
|
||||
export := flags.String("export", "", "directory to serve")
|
||||
readOnly := flags.Bool("ro", false, "serve the export read only: every mutation answers NFS4ERR_ROFS")
|
||||
rootSquash := flags.Bool("root-squash", false, "map a client claiming uid 0 onto nobody (65534), so root acts as the anonymous identity")
|
||||
tlsCert := flags.String("tls-cert", "", "certificate chain in PEM for RPC-with-TLS; requires -tls-key")
|
||||
tlsKey := flags.String("tls-key", "", "private key in PEM for RPC-with-TLS; requires -tls-cert")
|
||||
logOps := flags.Bool("log-ops", false, "log every operation with its status and duration to stderr")
|
||||
maxConns := flags.Int("max-connections", 0, "cap on live connections; a connection above the cap closes at once; 0 means no cap")
|
||||
stateDir := flags.String("state-dir", "", "directory for persisted client state: handles and opens survive a restart, and a grace window follows it")
|
||||
configPath := flags.String("config", "", "configuration file in TOML; never read unless named, the flags override it")
|
||||
version := flags.Bool("version", false, "print the version and exit")
|
||||
flags.Parse(os.Args[1:])
|
||||
|
||||
// The configuration file is the base, the flags override it: only
|
||||
// the flags present on the command line keep their value, everything
|
||||
// else yields to the file.
|
||||
if *configPath != "" {
|
||||
cfg, err := loadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
given := make(map[string]bool)
|
||||
flags.Visit(func(f *flag.Flag) { given[f.Name] = true })
|
||||
applyConfig(cfg, flags, func(name string) bool { return given[name] })
|
||||
}
|
||||
|
||||
if *version {
|
||||
fmt.Println(buildVersion())
|
||||
return
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if *export == "" {
|
||||
log.Fatalf("nfsd: no export given: pass -export DIR")
|
||||
}
|
||||
backend, err := nfsfs.NewLocal(*export)
|
||||
if err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
var fs nfsfs.FS = backend
|
||||
if *readOnly {
|
||||
fs = nfsfs.ReadOnly(backend)
|
||||
}
|
||||
if *stateDir != "" {
|
||||
if err := os.MkdirAll(*stateDir, 0o700); err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
// The handle map is the backend half of the recovery state: load
|
||||
// what a previous life wrote, then keep writing as handles are
|
||||
// minted, so a restart resolves what it served before.
|
||||
if err := backend.LoadPersistedHandles(*stateDir); err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
backend.SetPersistPath(*stateDir)
|
||||
}
|
||||
var tlsCfg *tls.Config
|
||||
if *tlsCert != "" || *tlsKey != "" {
|
||||
if *tlsCert == "" || *tlsKey == "" {
|
||||
log.Fatalf("nfsd: -tls-cert and -tls-key go together")
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(*tlsCert, *tlsKey)
|
||||
if err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
tlsCfg = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", *addr)
|
||||
if err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
log.Printf("nfsd: serving %s on %s", *export, ln.Addr())
|
||||
// The listener is the moment the service can answer: a Type=notify
|
||||
// unit learns it here.
|
||||
notifyReadyOrLog()
|
||||
|
||||
srv := &server.Server{
|
||||
Handle: (&nfs4server.Handler{
|
||||
FS: fs,
|
||||
TLSConfig: tlsCfg,
|
||||
LogOps: *logOps,
|
||||
StateDir: *stateDir,
|
||||
RootSquash: *rootSquash,
|
||||
}).HandleConn,
|
||||
MaxConns: *maxConns,
|
||||
}
|
||||
if err := srv.Serve(ctx, ln); err != nil {
|
||||
log.Fatalf("nfsd: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// buildVersion reports the module version the toolchain recorded at build
|
||||
// time. A build made at a tag reports the tag; a build outside version
|
||||
// control reports devel.
|
||||
func buildVersion() string {
|
||||
v := "devel"
|
||||
if bi, ok := debug.ReadBuildInfo(); ok && bi.Main.Version != "(devel)" {
|
||||
v = bi.Main.Version
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user