mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-25 09:44:57 +09:00
app: route ACME HTTP-01 listener through errgroup
The HTTP-01 challenge listener was launched in an orphan goroutine that called log.Fatal on bind failure, which bypassed the signal-handler shutdown path and made bind errors look identical to the main HTTP listener. Bind eagerly via net.ListenConfig in getTLSSettings, return the listener + http.Server in a tlsBundle, register the Serve loop with the existing errgroup, and call Shutdown alongside the other servers. Bind failures now surface as ListenerBindError(Listener:"ACME HTTP-01 challenge") through the normal error chain. Updates #3227
This commit is contained in:
+63
-21
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user