juanfont/headscale · critical

setting up socket: %w

Error message

setting up socket: %w

What it means

Returned when net.ListenConfig.Listen("unix", h.cfg.UnixSocket) fails at hscontrol/app.go:631, i.e. the operating system refused to bind a Unix domain socket at the configured path. Despite the generic wording, this is specifically the bind of the local admin/gRPC socket, not the TCP listener. The wrapped error is the raw errno from bind(2).

Source

Thrown at hscontrol/app.go:631

	//
	// Set up LOCAL listeners
	//

	err = h.ensureUnixSocketIsAbsent()
	if err != nil {
		return fmt.Errorf("removing old socket file: %w", err)
	}

	socketDir := filepath.Dir(h.cfg.UnixSocket)

	err = util.EnsureDir(socketDir)
	if err != nil {
		return fmt.Errorf("setting up unix socket: %w", err)
	}

	socketListener, err := new(net.ListenConfig).Listen(context.Background(), "unix", h.cfg.UnixSocket)
	if err != nil {
		return fmt.Errorf("setting up socket: %w", err)
	}

	// Change socket permissions
	if err := os.Chmod(h.cfg.UnixSocket, h.cfg.UnixSocketPermission); err != nil { //nolint:noinlineerr
		return fmt.Errorf("changing socket permission: %w", err)
	}

	// The Huma v1 API mux matches full /api/v1/... paths and is shared by
	// the local unix socket (served without authentication, local trust)
	// and the remote TCP router (served behind the API-key middleware).
	humaMux, _ := apiv1.Handler(apiv1.Backend{
		State:  h.state,
		Change: h.Change,
		Cfg:    h.cfg,
	})

	// The Headscale v2 API. Served behind Basic/Bearer auth on the remote
	// listener, and over the local unix socket (local trust) so the CLI can

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Re-run: in most race/stale-file cases the next start succeeds because ensureUnixSocketIsAbsent removes the offender; if not, manually rm the socket path.
  2. Shorten the socket path under 108 bytes total (e.g. /var/run/headscale/headscale.sock instead of deeply nested tmp dirs).
  3. Ensure the runtime user has write access to the socket directory (see also the 'setting up unix socket' error for the directory itself).
  4. Move the socket off network/unsupported filesystems onto local disk or a tmpfs.

Example fix

# before
unix_socket: /mnt/shared/nfs/headscale/headscale.sock   # NFS: bind fails

# after
unix_socket: /var/run/headscale/headscale.sock
Defensive patterns

Strategy: validation

Validate before calling

// Keep the unix socket path within the kernel sun_path limit.
func socketPathValid(p string) bool { return len(p) < 108 }

// Fail fast if the path is on a filesystem that cannot host sockets.
func probeUnixSocketFS(dir string) error {
    probe := filepath.Join(dir, ".probe.sock")
    l, err := net.Listen("unix", probe)
    if err != nil { return err }
    l.Close()
    return os.Remove(probe)
}

Try / catch

if err := h.Serve(); err != nil {
    if errors.Is(err, syscall.EADDRINUSE) || strings.Contains(err.Error(), "setting up socket") {
        // another instance or unsupported fs: stop instead of retry-looping
    }
}

Prevention

When it happens

Trigger: The path was recreated between the removal step and bind (race with another process); path length exceeds the ~108-byte sun_path limit (EINVAL/ENAMETOOLONG); the parent directory is not writable (EACCES); the filesystem does not support Unix sockets, e.g. unix_socket on an NFS/SMB/vBoxsf mount (EINVAL/ENODEV); a file with that name appeared and bind fails with EADDRINUSE.

Common situations: Two headscale instances starting concurrently with the same socket; unix_socket set to a long path (deep tmpdir in CI or macOS runner paths); socket placed on a network mount or a Windows bind-mount in Docker; a non-socket file created at the path by a script after cleanup.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/3204864f442abd34. Report an issue: GitHub.