juanfont/headscale · critical

STUN address not set

Error message

STUN address not set

What it means

Headscale returns this sentinel from Headscale.Serve() (hscontrol/app.go:552) when the embedded DERP server is enabled (derp.server.enabled) but derp.server.stun_addr is empty. The embedded DERP relay always needs a STUN server, so the config combination is rejected at startup. Nothing is started; the process fails fast before serving.

Source

Thrown at hscontrol/app.go:53

	"github.com/juanfont/headscale/hscontrol/state"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/juanfont/headscale/hscontrol/types/change"
	"github.com/juanfont/headscale/hscontrol/util"
	"github.com/pkg/profile"
	"github.com/rs/zerolog/log"
	"github.com/sasha-s/go-deadlock"
	"golang.org/x/crypto/acme"
	"golang.org/x/crypto/acme/autocert"
	"golang.org/x/sync/errgroup"
	"tailscale.com/envknob"
	"tailscale.com/tailcfg"
	"tailscale.com/types/dnstype"
	"tailscale.com/types/key"
	"tailscale.com/util/dnsname"
)

var (
	errSTUNAddressNotSet                   = errors.New("STUN address not set")
	errUnsupportedLetsEncryptChallengeType = errors.New(
		"unknown value for Lets Encrypt challenge type",
	)
	errEmptyInitialDERPMap = errors.New(
		"initial DERPMap is empty, Headscale requires at least one entry",
	)
)

var (
	debugDeadlock        = envknob.Bool("HEADSCALE_DEBUG_DEADLOCK")
	debugDeadlockTimeout = envknob.RegisterDuration("HEADSCALE_DEBUG_DEADLOCK_TIMEOUT")
)

func init() {
	deadlock.Opts.Disable = !debugDeadlock
	if debugDeadlock {
		deadlock.Opts.DeadlockTimeout = debugDeadlockTimeout()
		deadlock.Opts.PrintAllCurrentGoroutines = true

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Set derp.server.stun_addr in config.yaml (e.g. stun_addr: 0.0.0.0:3478) and ensure the UDP port is open on the host firewall
  2. If you do not want an embedded DERP/STUN server, set derp.server.enabled: false instead and rely on derp.urls / external regions
  3. Validate config with `headscale configtest` (or dump the parsed DERP section) before restarting the service

Example fix

# before
derp:
  server:
    enabled: true
    # stun_addr missing

# after
derp:
  server:
    enabled: true
    region_id: 999
    region_code: headscale
    region_name: Headscale Embedded DERP
    stun_addr: 0.0.0.0:3478
Defensive patterns

Strategy: validation

Validate before calling

# before starting headscale, assert the DERP/STUN config pair:
if grep -q 'enabled: true' <<< "$(yq '.derp.server' /etc/headscale/config.yaml)"; then
  [ -n "$(yq '.derp.server.stun_addr' /etc/headscale/config.yaml)" ] \
    || { echo 'derp.server.stun_addr must be set when derp.server.enabled'; exit 1; }
fi

Try / catch

if err := h.Serve(); err != nil {
    if errors.Is(err, errSTUNAddressNotSet) {
        // config bug: fail fast with a clear operator message
        log.Fatal().Msg("set derp.server.stun_addr (e.g. 0.0.0.0:3478) or disable derp.server")
    }
    return err
}

Prevention

When it happens

Trigger: Config file with derp.server.enabled: true and derp.server.stun_addr unset or "". Any invocation of headscale serve (or NewHeadscaleApp + Serve) with that combination returns errSTUNAddressNotSet at hscontrol/app.go:551-553.

Common situations: Enabling the embedded DERP server after a minimal config generated from an old example; YAML indentation mistakes that leave stun_addr nested under the wrong key; copying a config that only uses external DERP relays and then flipping server.enabled without adding the STUN block.

Related errors


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