// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // Package server accepts connections from NFS clients. // // Each accepted connection reaches the Handle hook, which owns the // connection for its whole lifetime; a nil Handle closes it at once. The // nfsd command wires the hook to the NFSv4.2 dispatcher. package server import ( "context" "errors" "net" "sync/atomic" "syscall" "time" ) // acceptRetryPause is the pause before the next accept after a // transient resource error, so a connection storm cannot spin the loop. const acceptRetryPause = 10 * time.Millisecond // A Server accepts connections from NFS clients. type Server struct { // Handle serves one accepted connection. It runs on its own goroutine // and owns the connection for its whole lifetime, closing it when the // session ends. A nil Handle closes the connection at once. Handle func(conn net.Conn) // MaxConns caps the connections served at once. Zero means no cap. A // connection offered above the cap closes at once, and the client // sees an immediate end of file. MaxConns int live atomic.Int64 } // transientAccept reports whether the accept error is survivable: the // listener stays usable and the next accept is worth trying. A storm of // aborted connections or a momentary file table exhaustion must not // take the daemon down with every client on it. func transientAccept(err error) bool { return errors.Is(err, syscall.ECONNABORTED) || errors.Is(err, syscall.EMFILE) || errors.Is(err, syscall.ENFILE) || errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EINTR) } // Serve accepts connections on ln until the listener fails or ctx is // cancelled. A cancellation closes the listener and Serve returns nil; // a closed listener returns nil; a transient accept error is waited out; // any other listener failure returns the error as is. In flight // connections are not drained: NFS clients retry through their session // replay caches, so an immediate return is the correct shutdown. func (s *Server) Serve(ctx context.Context, ln net.Listener) error { stop := make(chan struct{}) defer close(stop) go func() { select { case <-ctx.Done(): ln.Close() case <-stop: } }() for { conn, err := ln.Accept() if err == nil { if s.Handle == nil { conn.Close() continue } if s.MaxConns > 0 && s.live.Load() >= int64(s.MaxConns) { conn.Close() continue } s.live.Add(1) go func() { defer s.live.Add(-1) s.Handle(conn) }() continue } if ctx.Err() != nil || errors.Is(err, net.ErrClosed) { return nil } if transientAccept(err) { time.Sleep(acceptRetryPause) continue } return err } }