// Copyright (c) 2026 Petr BalvĂ­n (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 }