From 904f9fddccf56b646e187dc07afde0075c8514ff Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Fri, 17 Apr 2026 12:25:29 +1000 Subject: [PATCH] tls: centralise HTTPS RR ALPN defaults and registration Reuse shared protocol defaults instead of repeating the default HTTP protocol list, unify server name registration to carry ALPN in one experimental API and reuse the TLS default ALPN ordering for HTTPS RR publication --- modules/caddyhttp/app.go | 3 +- modules/caddyhttp/autohttps.go | 17 ++++------ modules/caddyhttp/server.go | 14 ++++++-- modules/caddytls/connpolicy.go | 4 +-- modules/caddytls/ech_dns_test.go | 9 +++-- modules/caddytls/tls.go | 58 +++++++++++++++----------------- 6 files changed, 55 insertions(+), 50 deletions(-) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 74f1466be..7b7b02e5d 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -23,6 +23,7 @@ import ( "maps" "net" "net/http" + "slices" "strconv" "sync" "time" @@ -235,7 +236,7 @@ func (app *App) Provision(ctx caddy.Context) error { // if no protocols configured explicitly, enable all except h2c if len(srv.Protocols) == 0 { - srv.Protocols = []string{"h1", "h2", "h3"} + srv.Protocols = slices.Clone(srv.protocolsWithDefaults()) } srvProtocolsUnique := map[string]struct{}{} diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 05ef08e1a..64240d9f4 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er for d := range serverDomainSet { echDomains = append(echDomains, d) } - app.tlsApp.RegisterServerNamesWithALPN(echDomains, httpsRRALPNs(srv)) + app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv)) // nothing more to do here if there are no domains that qualify for // automatic HTTPS and there are no explicit TLS connection policies: @@ -553,10 +553,7 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route { func httpsRRALPNs(srv *Server) []string { // Automatic HTTPS runs before server provisioning fills in the default // protocols, so derive the effective set directly from the raw config here. - serverProtocols := srv.Protocols - if len(serverProtocols) == 0 { - serverProtocols = []string{"h1", "h2", "h3"} - } + serverProtocols := srv.protocolsWithDefaults() protocols := make(map[string]struct{}, len(serverProtocols)) if srv.ListenProtocols == nil { @@ -583,17 +580,17 @@ func httpsRRALPNs(srv *Server) []string { } } - alpn := make([]string, 0, 3) + alpn := make(map[string]struct{}, 3) if _, ok := protocols["h3"]; ok { - alpn = append(alpn, "h3") + alpn["h3"] = struct{}{} } if _, ok := protocols["h2"]; ok { - alpn = append(alpn, "h2") + alpn["h2"] = struct{}{} } if _, ok := protocols["h1"]; ok { - alpn = append(alpn, "http/1.1") + alpn["http/1.1"] = struct{}{} } - return alpn + return caddytls.OrderedHTTPSRRALPN(alpn) } // createAutomationPolicies ensures that automated certificates for this diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 41a8e55b0..ec9fe18aa 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -301,6 +301,8 @@ type Server struct { onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) } +var defaultProtocols = []string{"h1", "h2", "h3"} + var ( ServerHeader = "Caddy" serverHeader = []string{ServerHeader} @@ -900,13 +902,14 @@ func (s *Server) logRequest( // protocol returns true if the protocol proto is configured/enabled. func (s *Server) protocol(proto string) bool { if s.ListenProtocols == nil { - if slices.Contains(s.Protocols, proto) { + if slices.Contains(s.protocolsWithDefaults(), proto) { return true } } else { + serverProtocols := s.protocolsWithDefaults() for _, lnProtocols := range s.ListenProtocols { for _, lnProtocol := range lnProtocols { - if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto { + if lnProtocol == "" && slices.Contains(serverProtocols, proto) || lnProtocol == proto { return true } } @@ -916,6 +919,13 @@ func (s *Server) protocol(proto string) bool { return false } +func (s *Server) protocolsWithDefaults() []string { + if len(s.Protocols) == 0 { + return defaultProtocols + } + return s.Protocols +} + // Listeners returns the server's listeners. These are active listeners, // so calling Accept() or Close() on them will probably break things. // They are made available here for read-only purposes (e.g. Addr()) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index c9258da48..9597af359 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -153,9 +153,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // in its config (remember, TLS connection policies are used by *other* apps to // run TLS servers) -- we skip names with placeholders if tlsApp.EncryptedClientHello.Publication == nil { - var echNames []string repl := caddy.NewReplacer() for _, p := range cp { + var echNames []string for _, m := range p.matchers { if sni, ok := m.(MatchServerName); ok { for _, name := range sni { @@ -164,8 +164,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { } } } + tlsApp.RegisterServerNames(echNames, p.ALPN) } - tlsApp.RegisterServerNames(echNames) } tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { diff --git a/modules/caddytls/ech_dns_test.go b/modules/caddytls/ech_dns_test.go index 6f555acc9..7c337366e 100644 --- a/modules/caddytls/ech_dns_test.go +++ b/modules/caddytls/ech_dns_test.go @@ -11,17 +11,16 @@ import ( func TestRegisterServerNamesWithALPN(t *testing.T) { tlsApp := &TLS{ - serverNames: make(map[string]struct{}), - serverNameALPN: make(map[string]map[string]struct{}), - serverNamesMu: new(sync.Mutex), + serverNames: make(map[string]serverNameRegistration), + serverNamesMu: new(sync.Mutex), } - tlsApp.RegisterServerNamesWithALPN([]string{ + tlsApp.RegisterServerNames([]string{ "Example.com:443", "example.com", "127.0.0.1:443", }, []string{"h2", "http/1.1"}) - tlsApp.RegisterServerNamesWithALPN([]string{"EXAMPLE.COM"}, []string{"h3"}) + tlsApp.RegisterServerNames([]string{"EXAMPLE.COM"}, []string{"h3"}) got := tlsApp.alpnValuesForServerNames([]string{"example.com:443", "127.0.0.1:443"}) want := map[string][]string{ diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 6a2f8f587..e5f6e6fc0 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -141,9 +141,8 @@ type TLS struct { logger *zap.Logger events *caddyevents.App - serverNames map[string]struct{} - serverNameALPN map[string]map[string]struct{} - serverNamesMu *sync.Mutex + serverNames map[string]serverNameRegistration + serverNamesMu *sync.Mutex // set of subjects with managed certificates, // and hashes of manually-loaded certificates @@ -170,8 +169,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.logger = ctx.Logger() repl := caddy.NewReplacer() t.managing, t.loaded = make(map[string]string), make(map[string]string) - t.serverNames = make(map[string]struct{}) - t.serverNameALPN = make(map[string]map[string]struct{}) + t.serverNames = make(map[string]serverNameRegistration) t.serverNamesMu = new(sync.Mutex) // set up default DNS module, if any, and make sure it implements all the @@ -651,24 +649,16 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str return false } -// RegisterServerNames registers the provided DNS names with the TLS app. -// This is currently used to auto-publish Encrypted ClientHello (ECH) -// configurations, if enabled. Use of this function by apps using the TLS -// app removes the need for the user to redundantly specify domain names -// in their configuration. This function separates hostname and port -// (keeping only the hotsname) and filters IP addresses, which can't be -// used with ECH. +// RegisterServerNames registers the provided DNS names with the TLS app and +// associates them with the given HTTPS RR ALPN values, if any. This is +// currently used to auto-publish Encrypted ClientHello (ECH) configurations, +// if enabled. Use of this function by apps using the TLS app removes the need +// for the user to redundantly specify domain names in their configuration. +// This function separates hostname and port, keeping only the hostname, and +// filters IP addresses which can't be used with ECH. // // EXPERIMENTAL: This function and its semantics/behavior are subject to change. -func (t *TLS) RegisterServerNames(dnsNames []string) { - t.RegisterServerNamesWithALPN(dnsNames, nil) -} - -// RegisterServerNamesWithALPN registers the provided DNS names with the TLS app -// and associates them with the given HTTPS RR ALPN values, if any. -// -// EXPERIMENTAL: This function and its semantics/behavior are subject to change. -func (t *TLS) RegisterServerNamesWithALPN(dnsNames []string, alpnValues []string) { +func (t *TLS) RegisterServerNames(dnsNames, alpnValues []string) { t.serverNamesMu.Lock() defer t.serverNamesMu.Unlock() @@ -681,21 +671,24 @@ func (t *TLS) RegisterServerNamesWithALPN(dnsNames []string, alpnValues []string if host == "" || certmagic.SubjectIsIP(host) { continue } - t.serverNames[host] = struct{}{} + + registration := t.serverNames[host] if len(alpnValues) == 0 { + t.serverNames[host] = registration continue } - if t.serverNameALPN[host] == nil { - t.serverNameALPN[host] = make(map[string]struct{}, len(alpnValues)) + if registration.alpnValues == nil { + registration.alpnValues = make(map[string]struct{}, len(alpnValues)) } for _, alpn := range alpnValues { if alpn == "" { continue } - t.serverNameALPN[host][alpn] = struct{}{} + registration.alpnValues[alpn] = struct{}{} } + t.serverNames[host] = registration } } @@ -714,22 +707,23 @@ func (t *TLS) alpnValuesForServerNames(dnsNames []string) map[string][]string { continue } - alpnSet := t.serverNameALPN[host] - if len(alpnSet) == 0 { + registration, ok := t.serverNames[host] + if !ok || len(registration.alpnValues) == 0 { continue } - result[host] = orderedHTTPSRRALPN(alpnSet) + result[host] = OrderedHTTPSRRALPN(registration.alpnValues) } return result } -func orderedHTTPSRRALPN(alpnSet map[string]struct{}) []string { +// OrderedHTTPSRRALPN returns the HTTPS RR ALPN values in preferred order. +func OrderedHTTPSRRALPN(alpnSet map[string]struct{}) []string { if len(alpnSet) == 0 { return nil } - knownOrder := []string{"h3", "h2", "http/1.1"} + knownOrder := append([]string{"h3"}, defaultALPN...) ordered := make([]string, 0, len(alpnSet)) seen := make(map[string]struct{}, len(alpnSet)) @@ -756,6 +750,10 @@ func orderedHTTPSRRALPN(alpnSet map[string]struct{}) []string { return append(ordered, remaining...) } +type serverNameRegistration struct { + alpnValues map[string]struct{} +} + // HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP // validation request is handled for the certificate named by r.Host, if it // is an HTTP challenge request. It requires that the automation policy for