Files
nfs/cmd/nfsd/main.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

132 lines
4.2 KiB
Go

// 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
}