{"record":{"id":"918eb702131af10b","repo":"tailscale/tailscale","slug":"server-closed","errorCode":null,"errorMessage":"server closed","messagePattern":"server closed","errorType":"exception","errorClass":"ErrServerClosed","httpStatus":null,"severity":"warning","filePath":"net/udprelay/server.go","lineNumber":965,"sourceCode":"\t\t\t// Write the packet batches via the socket associated with the\n\t\t\t// destination's address family. If source and destination address\n\t\t\t// families are matching we tx on the same socket the packet was\n\t\t\t// received, otherwise we use the \"other\" socket. [Server] makes no\n\t\t\t// use of dual-stack sockets.\n\t\t\tif dest.Addr().Is4() == readFromSocketIsIPv4 {\n\t\t\t\treadFromSocket.WriteBatchTo(buffs, dest, packet.GeneveHeader{}, 0)\n\t\t\t} else {\n\t\t\t\totherSocket.WriteBatchTo(buffs, dest, packet.GeneveHeader{}, 0)\n\t\t\t}\n\t\t\tdelete(writeBuffsByDest, dest)\n\t\t}\n\n\t\ts.metrics.countForwarded(readFromSocketIsIPv4, true, forwardedByOutAF.bytes4, forwardedByOutAF.packets4)\n\t\ts.metrics.countForwarded(readFromSocketIsIPv4, false, forwardedByOutAF.bytes6, forwardedByOutAF.packets6)\n\t}\n}\n\nvar ErrServerClosed = errors.New(\"server closed\")\n\n// ErrServerNotReady indicates the server is not ready. Allocation should be\n// requested after waiting for at least RetryAfter duration.\ntype ErrServerNotReady struct {\n\tRetryAfter time.Duration\n}\n\nfunc (e ErrServerNotReady) Error() string {\n\treturn fmt.Sprintf(\"server not ready, retry after %v\", e.RetryAfter)\n}\n\n// getNextVNILocked returns the next available VNI. It implements the\n// \"Traditional BSD Port Selection Algorithm\" from RFC6056. This algorithm does\n// not attempt to obfuscate the selection, i.e. the selection is predictable.\n// For now, we favor simplicity and reducing VNI re-use over more complex\n// ephemeral port (VNI) selection algorithms.\nfunc (s *Server) getNextVNILocked() (uint32, error) {\n\tfor range totalPossibleVNI {","sourceCodeStart":947,"sourceCodeEnd":983,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/net/udprelay/server.go#L947-L983","documentation":"ErrServerClosed is the udprelay Server's shutdown sentinel, the analogue of http.ErrServerClosed. It is returned from server operations (relay loops, reads/writes on allocated endpoints) once the Server has been closed, signaling callers to stop rather than retry.","triggerScenarios":"Invoking Serve/relay operations after Close/Shutdown, or racing a Close against an in-flight operation so the operation observes the closed state.","commonSituations":"Goroutines that keep using the server after shutdown was initiated; missing synchronization between the shutdown path and workers; tests that close the server then drain results.","solutions":["Treat ErrServerClosed as a normal stop signal: exit the goroutine instead of retrying or logging it as a failure","Audit shutdown ordering so workers finish before Close, or select on a done channel alongside server operations","Wrap the check with errors.Is(err, udprelay.ErrServerClosed) at the top of your error handling"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"// isServerClosed reports whether err is the udprelay shutdown sentinel.\nfunc isServerClosed(err error) bool { return errors.Is(err, udprelay.ErrServerClosed) }","tryCatchPattern":"if err := srv.Serve(ctx); err != nil && !errors.Is(err, udprelay.ErrServerClosed) {\n\tlog.Fatalf(\"relay failed: %v\", err)\n}\n// ErrServerClosed is the normal shutdown path: return nil","preventionTips":["Structure relay workers to select on a shutdown channel so they stop before Close","Never log ErrServerClosed as an error; treat it like http.ErrServerClosed"],"tags":["go","udp","relay","shutdown","sentinel-error"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}