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
132 lines
4.2 KiB
Go
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
|
|
}
|