diff --git a/hscontrol/app.go b/hscontrol/app.go index 6d6a93e2..205026b9 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -678,11 +678,13 @@ func (h *Headscale) Serve() error { // Set up REMOTE listeners // - tlsConfig, err := h.getTLSSettings() + tlsBundle, err := h.getTLSSettings(ctx) if err != nil { return fmt.Errorf("configuring TLS settings: %w", err) } + tlsConfig := tlsBundle.Config + // // // HTTP setup @@ -724,6 +726,20 @@ func (h *Headscale) Serve() error { log.Info(). Msgf("listening and serving HTTP on: %s", h.cfg.Addr) + if tlsBundle.ACMEServer != nil { + log.Info().Msgf( + "listening and serving ACME HTTP-01 challenge on: %s", + tlsBundle.ACMEListener.Addr()) + errorGroup.Go(func() error { + err := tlsBundle.ACMEServer.Serve(tlsBundle.ACMEListener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("ACME HTTP-01 challenge listener: %w", err) + } + + return nil + }) + } + // Only start debug/metrics server if address is configured var debugHTTPServer *http.Server @@ -838,6 +854,17 @@ func (h *Headscale) Serve() error { log.Error().Err(err).Msg("failed to shutdown http") } + if tlsBundle.ACMEServer != nil { + info("shutting down ACME HTTP-01 challenge server") + + err := tlsBundle.ACMEServer.Shutdown(shutdownCtx) + if err != nil { + log.Error().Err(err).Msg("failed to shutdown ACME HTTP-01 server") + } + + tlsBundle.ACMEListener.Close() + } + info("closing batcher") h.mapBatcher.Close() @@ -893,7 +920,17 @@ func (h *Headscale) Serve() error { return errorGroup.Wait() } -func (h *Headscale) getTLSSettings() (*tls.Config, error) { +// tlsBundle carries the TLS settings produced by getTLSSettings. When +// HTTP-01 ACME is configured, ACMEServer and ACMEListener are populated +// so the caller can register the challenge listener with the errgroup +// and wire it into the shutdown path. Otherwise both are nil. +type tlsBundle struct { + Config *tls.Config + ACMEServer *http.Server + ACMEListener net.Listener +} + +func (h *Headscale) getTLSSettings(ctx context.Context) (*tlsBundle, error) { tlsEnabled := h.cfg.TLS.LetsEncrypt.Hostname != "" || h.cfg.TLS.CertPath != "" if tlsEnabled && !strings.HasPrefix(h.cfg.ServerURL, "https://") { log.Warn().Msg("listening with TLS but ServerURL does not start with https://") @@ -922,7 +959,7 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) { // Configuration via autocert with TLS-ALPN-01 (https://tools.ietf.org/html/rfc8737) // The RFC requires that the validation is done on port 443; in other words, headscale // must be reachable on port 443. - return certManager.TLSConfig(), nil + return &tlsBundle{Config: certManager.TLSConfig()}, nil case types.HTTP01ChallengeType: // Configuration via autocert with HTTP-01. This requires listening on @@ -934,15 +971,21 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) { ReadTimeout: types.HTTPTimeout, } - go func() { - err := server.ListenAndServe() - log.Fatal(). - Caller(). - Err(err). - Msg("failed to set up a HTTP server") - }() + listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", server.Addr) + if err != nil { + return nil, &types.ListenerBindError{ + Listener: "ACME HTTP-01 challenge", + YAMLKey: "tls_letsencrypt_listen", + Addr: server.Addr, + Err: err, + } + } - return certManager.TLSConfig(), nil + return &tlsBundle{ + Config: certManager.TLSConfig(), + ACMEServer: server, + ACMEListener: listener, + }, nil default: return nil, errUnsupportedLetsEncryptChallengeType @@ -950,23 +993,22 @@ func (h *Headscale) getTLSSettings() (*tls.Config, error) { } if h.cfg.TLS.CertPath == "" { - return nil, nil //nolint:nilnil // intentional: no TLS config when neither LetsEncrypt nor a cert path is set - } - - tlsConfig := &tls.Config{ - NextProtos: []string{"http/1.1"}, - Certificates: make([]tls.Certificate, 1), - MinVersion: tls.VersionTLS12, + return &tlsBundle{}, nil } cert, err := tls.LoadX509KeyPair(h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath) if err != nil { - return nil, err + return nil, fmt.Errorf("loading TLS keypair (tls_cert_path=%q, tls_key_path=%q): %w", + h.cfg.TLS.CertPath, h.cfg.TLS.KeyPath, err) } - tlsConfig.Certificates[0] = cert + tlsConfig := &tls.Config{ + NextProtos: []string{"http/1.1"}, + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } - return tlsConfig, nil + return &tlsBundle{Config: tlsConfig}, nil } func readOrCreatePrivateKey(path string) (*key.MachinePrivate, error) {