Files
headscale/cmd/headscale/cli/serve.go
T
Kristoffer Dalby 2114ced0d5 cli: classify Serve errors with operator hints
A *ListenerBindError that wraps syscall.EADDRINUSE now ends with a
"sudo ss -tlnp 'sport = :PORT'" pointer, and one wrapping
syscall.EACCES with a CAP_NET_BIND_SERVICE / setcap pointer. The
underlying chain is preserved via fmt.Errorf("%w"), so errors.Is /
errors.As continue to walk to the typed bind error and the syscall
errno.

Drop the "headscale ran into an error and had to shut down" wrap,
which only restated the symptom.

Export types.PortFromAddr so the classifier can render the port
number in the hint.

Updates #3227
2026-09-23 15:18:34 +02:00

76 lines
1.8 KiB
Go

package cli
import (
"errors"
"fmt"
"net/http"
"syscall"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/spf13/cobra"
"github.com/tailscale/squibble"
)
func init() {
rootCmd.AddCommand(serveCmd)
}
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Launches the headscale server",
RunE: func(cmd *cobra.Command, args []string) error {
app, err := newHeadscaleServerWithConfig()
if err != nil {
if squibbleErr, ok := errors.AsType[squibble.ValidationError](err); ok {
fmt.Printf("SQLite schema failed to validate:\n")
fmt.Println(squibbleErr.Diff)
}
return fmt.Errorf("initializing: %w", err)
}
err = app.Serve()
if err == nil || errors.Is(err, http.ErrServerClosed) {
return nil
}
return classifyServeError(err)
},
}
// classifyServeError augments specific error classes with operator
// hints. The underlying chain is left intact so errors.Is / errors.As
// continue to walk to ListenerBindError, syscall.EADDRINUSE, etc.
func classifyServeError(err error) error {
var bindErr *types.ListenerBindError
if !errors.As(err, &bindErr) {
return err
}
switch {
case errors.Is(err, syscall.EADDRINUSE):
port, perr := types.PortFromAddr(bindErr.Addr)
if perr != nil {
return fmt.Errorf(
"%w\n\nHint: another process on this host is bound to the same address. "+
"Find it with: sudo ss -tlnp",
err)
}
return fmt.Errorf(
"%w\n\nHint: another process on this host is bound to the same address. "+
"Find it with: sudo ss -tlnp 'sport = :%d'",
err, port)
case errors.Is(err, syscall.EACCES):
return fmt.Errorf(
"%w\n\nHint: binding to a privileged port (<1024) requires root or "+
"CAP_NET_BIND_SERVICE. The shipped systemd unit grants this capability; "+
"if running manually, use sudo or "+
"`setcap cap_net_bind_service=+ep ./headscale`",
err)
}
return err
}