diff --git a/admin.go b/admin.go index 6df5a23f7..244c7d719 100644 --- a/admin.go +++ b/admin.go @@ -221,7 +221,8 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co if remote { muxWrap.remoteControl = admin.Remote } else { - muxWrap.enforceHost = !addr.isWildcardInterface() + // see comment in allowedOrigins() as to why we disable the host check for unix/fd networks + muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork() muxWrap.allowedOrigins = admin.allowedOrigins(addr) muxWrap.enforceOrigin = admin.EnforceOrigin } @@ -310,47 +311,43 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL { for _, o := range admin.Origins { uniqueOrigins[o] = struct{}{} } - if admin.Origins == nil { + // RFC 2616, Section 14.26: + // "A client MUST include a Host header field in all HTTP/1.1 request + // messages. If the requested URI does not include an Internet host + // name for the service being requested, then the Host header field MUST + // be given with an empty value." + // + // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6. + // Understandable, but frustrating. See: + // https://github.com/golang/go/issues/60374 + // See also the discussion here: + // https://github.com/golang/go/issues/61431 + // + // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl + // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a + // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin + // security checks, the infosec community assures me that it is secure to do + // so, because: + // + // 1) Browsers do not allow access to unix sockets + // 2) DNS is irrelevant to unix sockets + // + // If either of those two statements ever fail to hold true, it is not the + // fault of Caddy. + // + // Thus, we do not fill out allowed origins and do not enforce Host + // requirements for unix sockets. Enforcing it leads to confusion and + // frustration, when UDS have their own permissions from the OS. + // Enforcing host requirements here is effectively security theater, + // and a false sense of security. + // + // See also the discussion in #6832. + if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() { if addr.isLoopback() { - if addr.IsUnixNetwork() || addr.IsFdNetwork() { - // RFC 2616, Section 14.26: - // "A client MUST include a Host header field in all HTTP/1.1 request - // messages. If the requested URI does not include an Internet host - // name for the service being requested, then the Host header field MUST - // be given with an empty value." - // - // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6. - // Understandable, but frustrating. See: - // https://github.com/golang/go/issues/60374 - // See also the discussion here: - // https://github.com/golang/go/issues/61431 - // - // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl - // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a - // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin - // security checks, the infosec community assures me that it is secure to do - // so, because: - // 1) Browsers do not allow access to unix sockets - // 2) DNS is irrelevant to unix sockets - // - // I am not quite ready to trust either of those external factors, so instead - // of disabling Host/Origin checks, we now allow specific Host values when - // accessing the admin endpoint over unix sockets. I definitely don't trust - // DNS (e.g. I don't trust 'localhost' to always resolve to the local host), - // and IP shouldn't even be used, but if it is for some reason, I think we can - // at least be reasonably assured that 127.0.0.1 and ::1 route to the local - // machine, meaning that a hypothetical browser origin would have to be on the - // local machine as well. - uniqueOrigins[""] = struct{}{} - uniqueOrigins["127.0.0.1"] = struct{}{} - uniqueOrigins["::1"] = struct{}{} - } else { - uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} - uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} - uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} - } - } - if !addr.IsUnixNetwork() && !addr.IsFdNetwork() { + uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} + uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} + uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} + } else { uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} } } diff --git a/admin_test.go b/admin_test.go index b00cfaae2..e3d235b66 100644 --- a/admin_test.go +++ b/admin_test.go @@ -531,6 +531,7 @@ func TestAdminRouterProvisioning(t *testing.T) { } func TestAllowedOriginsUnixSocket(t *testing.T) { + // see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS tests := []struct { name string addr NetworkAddress @@ -543,12 +544,8 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { Network: "unix", Host: "/tmp/caddy.sock", }, - origins: nil, // default origins - expectOrigins: []string{ - "", // empty host as per RFC 2616 - "127.0.0.1", - "::1", - }, + origins: nil, // default origins + expectOrigins: []string{}, }, { name: "unix socket with custom origins", @@ -578,7 +575,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { }, } - for _, test := range tests { + for i, test := range tests { t.Run(test.name, func(t *testing.T) { admin := AdminConfig{ Origins: test.origins, @@ -592,7 +589,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { } if len(gotOrigins) != len(test.expectOrigins) { - t.Errorf("Expected %d origins but got %d", len(test.expectOrigins), len(gotOrigins)) + t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins)) return } @@ -607,7 +604,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) { } if !reflect.DeepEqual(expectMap, gotMap) { - t.Errorf("Origins mismatch.\nExpected: %v\nGot: %v", test.expectOrigins, gotOrigins) + t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins) } }) } diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index ae6f5ddee..3dcd3ea5b 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -633,12 +633,6 @@ func (st *ServerType) serversFromPairings( srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) } srv.AutoHTTPS.IgnoreLoadedCerts = true - - case "prefer_wildcard": - if srv.AutoHTTPS == nil { - srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) - } - srv.AutoHTTPS.PreferWildcard = true } } @@ -706,16 +700,6 @@ func (st *ServerType) serversFromPairings( return specificity(iLongestHost) > specificity(jLongestHost) }) - // collect all hosts that have a wildcard in them - wildcardHosts := []string{} - for _, sblock := range p.serverBlocks { - for _, addr := range sblock.parsedKeys { - if strings.HasPrefix(addr.Host, "*.") { - wildcardHosts = append(wildcardHosts, addr.Host[2:]) - } - } - } - var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled @@ -801,7 +785,13 @@ func (st *ServerType) serversFromPairings( cp.FallbackSNI = fallbackSNI } - // only append this policy if it actually changes something + // only append this policy if it actually changes something, + // or if the configuration explicitly automates certs for + // these names (this is necessary to hoist a connection policy + // above one that may manually load a wildcard cert that would + // otherwise clobber the automated one; the code that appends + // policies that manually load certs comes later, so they're + // lower in the list) if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) { srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) hasCatchAllTLSConnPolicy = len(hosts) == 0 @@ -841,18 +831,6 @@ func (st *ServerType) serversFromPairings( addressQualifiesForTLS = true } - // If prefer wildcard is enabled, then we add hosts that are - // already covered by the wildcard to the skip list - if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard { - baseDomain := addr.Host - if idx := strings.Index(baseDomain, "."); idx != -1 { - baseDomain = baseDomain[idx+1:] - } - if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) { - srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host) - } - } - // predict whether auto-HTTPS will add the conn policy for us; if so, we // may not need to add one for this server autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy && @@ -1083,11 +1061,40 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti // if they're exactly equal in every way, just keep one of them if reflect.DeepEqual(cps[i], cps[j]) { - cps = append(cps[:j], cps[j+1:]...) + cps = slices.Delete(cps, j, j+1) i-- break } + // as a special case, if there are adjacent TLS conn policies that are identical except + // by their matchers, and the matchers are specifically just ServerName ("sni") matchers + // (by far the most common), we can combine them into a single policy + if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 { + if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok { + if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok { + // position of policies and the matcher criteria check out; if settings are + // the same, then we can combine the policies; we have to unmarshal and + // remarshal the matchers though + if cps[i].SettingsEqual(*cps[j]) { + var iSNIMatcher caddytls.MatchServerName + if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil { + var jSNIMatcher caddytls.MatchServerName + if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil { + iSNIMatcher = append(iSNIMatcher, jSNIMatcher...) + cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher) + if err != nil { + return nil, fmt.Errorf("recombining SNI matchers: %v", err) + } + cps = slices.Delete(cps, j, j+1) + i-- + break + } + } + } + } + } + } + // if they have the same matcher, try to reconcile each field: either they must // be identical, or we have to be able to combine them safely if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) { @@ -1189,12 +1196,13 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti } } - cps = append(cps[:j], cps[j+1:]...) + cps = slices.Delete(cps, j, j+1) i-- break } } } + return cps, nil } diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 2eedca453..a0899dd8d 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -92,11 +92,9 @@ func (st ServerType) buildTLSApp( tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) } - // collect all hosts that have a wildcard in them, and arent HTTP - wildcardHosts := []string{} - // hosts that have been explicitly marked to be automated, - // even if covered by another wildcard - forcedAutomatedNames := make(map[string]struct{}) + var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP + forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard + for _, p := range pairings { var addresses []string for _, addressWithProtocols := range p.addressesWithProtocols { @@ -153,7 +151,7 @@ func (st ServerType) buildTLSApp( ap.OnDemand = true } - // collect hosts that are forced to be automated + // collect hosts that are forced to have certs automated for their specific name if _, ok := sblock.pile["tls.force_automate"]; ok { for _, host := range sblockHosts { forcedAutomatedNames[host] = struct{}{} @@ -375,7 +373,9 @@ func (st ServerType) buildTLSApp( return nil, warnings, err } for _, cfg := range ech.Configs { - ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) + if cfg.PublicName != "" { + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) + } } if tlsApp.Automation == nil { tlsApp.Automation = new(caddytls.AutomationConfig) diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest deleted file mode 100644 index 04f2c4665..000000000 --- a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest +++ /dev/null @@ -1,109 +0,0 @@ -{ - auto_https prefer_wildcard -} - -*.example.com { - tls { - dns mock - } - respond "fallback" -} - -foo.example.com { - respond "foo" -} ----------- -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "foo.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "foo", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "*.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "automatic_https": { - "skip_certificates": [ - "foo.example.com" - ], - "prefer_wildcard": true - } - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "*.example.com" - ], - "issuers": [ - { - "challenges": { - "dns": { - "provider": { - "name": "mock" - } - } - }, - "module": "acme" - } - ] - } - ] - } - } - } -} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest deleted file mode 100644 index 4f8c26a5d..000000000 --- a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest +++ /dev/null @@ -1,268 +0,0 @@ -{ - auto_https prefer_wildcard -} - -# Covers two domains -*.one.example.com { - tls { - dns mock - } - respond "one fallback" -} - -# Is covered, should not get its own AP -foo.one.example.com { - respond "foo one" -} - -# This one has its own tls config so it doesn't get covered (escape hatch) -bar.one.example.com { - respond "bar one" - tls bar@bar.com -} - -# Covers nothing but AP gets consolidated with the first -*.two.example.com { - tls { - dns mock - } - respond "two fallback" -} - -# Is HTTP so it should not cover -http://*.three.example.com { - respond "three fallback" -} - -# Has no wildcard coverage so it gets an AP -foo.three.example.com { - respond "foo three" -} ----------- -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "foo.three.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "foo three", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "foo.one.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "foo one", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "bar.one.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "bar one", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "*.one.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "one fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - }, - { - "match": [ - { - "host": [ - "*.two.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "two fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "automatic_https": { - "skip_certificates": [ - "foo.one.example.com", - "bar.one.example.com" - ], - "prefer_wildcard": true - } - }, - "srv1": { - "listen": [ - ":80" - ], - "routes": [ - { - "match": [ - { - "host": [ - "*.three.example.com" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "three fallback", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "automatic_https": { - "prefer_wildcard": true - } - } - } - }, - "tls": { - "automation": { - "policies": [ - { - "subjects": [ - "foo.three.example.com" - ] - }, - { - "subjects": [ - "bar.one.example.com" - ], - "issuers": [ - { - "email": "bar@bar.com", - "module": "acme" - }, - { - "ca": "https://acme.zerossl.com/v2/DV90", - "email": "bar@bar.com", - "module": "acme" - } - ] - }, - { - "subjects": [ - "*.one.example.com", - "*.two.example.com" - ], - "issuers": [ - { - "challenges": { - "dns": { - "provider": { - "name": "mock" - } - } - }, - "module": "acme" - } - ] - } - ] - } - } - } -} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest index 4eb6c4f1c..623bafd70 100644 --- a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest @@ -131,13 +131,7 @@ shadowed.example.com { { "match": { "sni": [ - "automated1.example.com" - ] - } - }, - { - "match": { - "sni": [ + "automated1.example.com", "automated2.example.com" ] } diff --git a/context.go b/context.go index a65814f03..eb0979f3a 100644 --- a/context.go +++ b/context.go @@ -577,11 +577,11 @@ func (ctx Context) Slogger() *slog.Logger { if err != nil { panic("config missing, unable to create dev logger: " + err.Error()) } - return slog.New(zapslog.NewHandler(l.Core(), nil)) + return slog.New(zapslog.NewHandler(l.Core())) } mod := ctx.Module() if mod == nil { - return slog.New(zapslog.NewHandler(Log().Core(), nil)) + return slog.New(zapslog.NewHandler(Log().Core())) } return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(), zapslog.WithName(string(mod.CaddyModule().ID)), diff --git a/go.mod b/go.mod index 6c5894540..2a2993aa9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.15.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.22.2 + github.com/caddyserver/certmagic v0.23.0 github.com/caddyserver/zerossl v0.1.3 github.com/cloudflare/circl v1.6.0 github.com/dustin/go-humanize v1.0.1 @@ -17,9 +17,9 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.0 github.com/klauspost/cpuid/v2 v2.2.10 - github.com/mholt/acmez/v3 v3.1.1 + github.com/mholt/acmez/v3 v3.1.2 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.50.1 + github.com/quic-go/quic-go v0.51.0 github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 @@ -39,7 +39,7 @@ require ( go.uber.org/zap/exp v0.3.0 golang.org/x/crypto v0.36.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 - golang.org/x/net v0.37.0 + golang.org/x/net v0.38.0 golang.org/x/sync v0.12.0 golang.org/x/term v0.30.0 golang.org/x/time v0.11.0 @@ -116,7 +116,7 @@ require ( github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/libdns/libdns v0.2.3 + github.com/libdns/libdns v1.0.0-beta.1 github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index e2c85355b..a53eacf81 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/caddyserver/certmagic v0.22.2 h1:qzZURXlrxwR5m25/jpvVeEyJHeJJMvAwe5zlMufOTQk= -github.com/caddyserver/certmagic v0.22.2/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs= +github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU= +github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -327,8 +327,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8= -github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= +github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -347,8 +347,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/mholt/acmez/v3 v3.1.1 h1:Jh+9uKHkPxUJdxM16q5mOr+G2V0aqkuFtNA28ihCxhQ= -github.com/mholt/acmez/v3 v3.1.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= +github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= @@ -397,8 +397,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= -github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc= +github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= @@ -633,8 +633,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/internal/logs.go b/internal/logs.go new file mode 100644 index 000000000..4ed4a572e --- /dev/null +++ b/internal/logs.go @@ -0,0 +1,22 @@ +package internal + +import "fmt" + +// MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length +// maxToDisplay. It is useful for logging domains being managed, for example, since a +// map is typically needed for quick lookup, but a slice is needed for logging, and this +// can be quite a doozy since there may be a huge amount (hundreds of thousands). +func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string { + numberOfNamesToDisplay := min(len(subjects), maxToDisplay) + domainsToDisplay := make([]string, 0, numberOfNamesToDisplay) + for domain := range subjects { + domainsToDisplay = append(domainsToDisplay, domain) + if len(domainsToDisplay) >= numberOfNamesToDisplay { + break + } + } + if len(subjects) > maxToDisplay { + domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay)) + } + return domainsToDisplay +} diff --git a/listeners.go b/listeners.go index b22df77ba..9e0057678 100644 --- a/listeners.go +++ b/listeners.go @@ -210,7 +210,7 @@ func (na NetworkAddress) IsUnixNetwork() bool { return IsUnixNetwork(na.Network) } -// IsUnixNetwork returns true if na.Network is +// IsFdNetwork returns true if na.Network is // fd or fdgram. func (na NetworkAddress) IsFdNetwork() bool { return IsFdNetwork(na.Network) @@ -641,7 +641,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) { if network == "tcp" || network == "tcp4" || network == "tcp6" || network == "udp" || network == "udp4" || network == "udp6" || network == "unix" || network == "unixpacket" || network == "unixgram" || - strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) || + strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") || network == "fd" || network == "fdgram" { panic("network type " + network + " is reserved") } diff --git a/logging.go b/logging.go index 6bc9f10f2..fffd57672 100644 --- a/logging.go +++ b/logging.go @@ -20,6 +20,7 @@ import ( "io" "log" "os" + "slices" "strings" "sync" "time" @@ -490,10 +491,8 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error { if len(cl.Include) > 0 && len(cl.Exclude) > 0 { // prevent intersections for _, allow := range cl.Include { - for _, deny := range cl.Exclude { - if allow == deny { - return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) - } + if slices.Contains(cl.Exclude, allow) { + return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) } } diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index cbd168d31..b550904e2 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -73,7 +73,7 @@ func init() { // `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on // `{http.request.local.port}` | The port part of the local address the connection arrived on // `{http.request.local}` | The local address the connection arrived on -// `{http.request.remote.host}` | The host (IP) part of the remote client's address +// `{http.request.remote.host}` | The host (IP) part of the remote client's address, if available (not known with HTTP/3 early data) // `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote}` | The address of the remote client // `{http.request.scheme}` | The request scheme, typically `http` or `https` @@ -152,7 +152,7 @@ type App struct { tlsApp *caddytls.TLS // used temporarily between phases 1 and 2 of auto HTTPS - allCertDomains []string + allCertDomains map[string]struct{} } // CaddyModule returns the Caddy module information. diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index f41d9e683..c34954f92 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -25,6 +25,7 @@ import ( "go.uber.org/zap" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/internal" "github.com/caddyserver/caddy/v2/modules/caddytls" ) @@ -65,12 +66,6 @@ type AutoHTTPSConfig struct { // enabled. To force automated certificate management // regardless of loaded certificates, set this to true. IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` - - // If true, automatic HTTPS will prefer wildcard names - // and ignore non-wildcard names if both are available. - // This allows for writing a config with top-level host - // matchers without having those names produce certificates. - PreferWildcard bool `json:"prefer_wildcard,omitempty"` } // automaticHTTPSPhase1 provisions all route matchers, determines @@ -163,33 +158,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } - // trim the list of domains covered by wildcards, if configured - if srv.AutoHTTPS.PreferWildcard { - wildcards := make(map[string]struct{}) - for d := range serverDomainSet { - if strings.HasPrefix(d, "*.") { - wildcards[d[2:]] = struct{}{} - } - } - for d := range serverDomainSet { - if strings.HasPrefix(d, "*.") { - continue - } - base := d - if idx := strings.Index(d, "."); idx != -1 { - base = d[idx+1:] - } - if _, ok := wildcards[base]; ok { - delete(serverDomainSet, d) - } - } - } - // build the list of domains that could be used with ECH (if enabled) - // so the TLS app can know to publish ECH configs for them; we do this - // after trimming domains covered by wildcards because, presumably, - // if the user wants to use wildcard certs, they also want to use the - // wildcard for ECH, rather than individual subdomains + // so the TLS app can know to publish ECH configs for them echDomains := make([]string, 0, len(serverDomainSet)) for d := range serverDomainSet { echDomains = append(echDomains, d) @@ -295,19 +265,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } - // we now have a list of all the unique names for which we need certs; - // turn the set into a slice so that phase 2 can use it - app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) + // we now have a list of all the unique names for which we need certs var internal, tailscale []string uniqueDomainsLoop: for d := range uniqueDomainsForCerts { - if !isTailscaleDomain(d) { - // whether or not there is already an automation policy for this - // name, we should add it to the list to manage a cert for it, - // unless it's a Tailscale domain, because we don't manage those - app.allCertDomains = append(app.allCertDomains, d) - } - // some names we've found might already have automation policies // explicitly specified for them; we should exclude those from // our hidden/implicit policy, since applying a name to more than @@ -346,6 +307,7 @@ uniqueDomainsLoop: } if isTailscaleDomain(d) { tailscale = append(tailscale, d) + delete(uniqueDomainsForCerts, d) // not managed by us; handled separately } else if shouldUseInternal(d) { internal = append(internal, d) } @@ -475,6 +437,9 @@ redirServersLoop: } } + // persist the domains/IPs we're managing certs for through provisioning/startup + app.allCertDomains = uniqueDomainsForCerts + logger.Debug("adjusted config", zap.Reflect("tls", app.tlsApp), zap.Reflect("http", app)) @@ -777,7 +742,7 @@ func (app *App) automaticHTTPSPhase2() error { return nil } app.logger.Info("enabling automatic TLS certificate management", - zap.Strings("domains", app.allCertDomains), + zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)), ) err := app.tlsApp.Manage(app.allCertDomains) if err != nil { diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index f799d7a0c..69db62a5c 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -37,6 +37,10 @@ func init() { // `{http.auth.user.*}` placeholders may be set for any authentication // modules that provide user metadata. // +// In case of an error, the placeholder `{http.auth..error}` +// will be set to the error message returned by the authentication +// provider. +// // Its API is still experimental and may be subject to change. type Authentication struct { // A set of authentication providers. If none are specified, @@ -71,6 +75,7 @@ func (a *Authentication) Provision(ctx caddy.Context) error { } func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) var user User var authed bool var err error @@ -80,6 +85,9 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil { c.Write(zap.String("provider", provName), zap.Error(err)) } + // Set the error from the authentication provider in a placeholder, + // so it can be used in the handle_errors directive. + repl.Set("http.auth."+provName+".error", err.Error()) continue } if authed { @@ -90,7 +98,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) } - repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl.Set("http.auth.user.id", user.ID) for k, v := range user.Metadata { repl.Set("http.auth.user."+k, v) diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index d2d698197..704661f21 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -26,7 +26,7 @@ - {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} + {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg" ".avif"}} {{- if eq .Tpl.Layout "grid"}} {{- else}} @@ -802,7 +802,7 @@ footer { {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} - {{.HumanTotalFileSize}} total + {{.HumanTotalFileSize}} total {{- if ne 0 .Limit}} @@ -868,7 +868,7 @@ footer { {{- end}} - + {{- if and (eq .Sort "name") (ne .Order "desc")}} Name diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index a27fc44f0..9da3bfefa 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -309,7 +309,9 @@ func (h *Handler) doActiveHealthCheckForAllHosts() { } }() - networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true) + repl := caddy.NewReplacer() + + networkAddr, err := repl.ReplaceOrErr(upstream.Dial, true, true) if err != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil { c.Write( @@ -344,14 +346,24 @@ func (h *Handler) doActiveHealthCheckForAllHosts() { return } hostAddr := addr.JoinHostPort(0) - dialAddr := hostAddr if addr.IsUnixNetwork() || addr.IsFdNetwork() { // this will be used as the Host portion of a http.Request URL, and // paths to socket files would produce an error when creating URL, // so use a fake Host value instead; unix sockets are usually local hostAddr = "localhost" } - err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream) + + // Fill in the dial info for the upstream + // If the upstream is set, use that instead + dialInfoUpstream := upstream + if h.HealthChecks.Active.Upstream != "" { + dialInfoUpstream = &Upstream{ + Dial: h.HealthChecks.Active.Upstream, + } + } + dialInfo, _ := dialInfoUpstream.fillDialInfo(repl) + + err = h.doActiveHealthCheck(dialInfo, hostAddr, networkAddr, upstream) if err != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil { c.Write( diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go index 76cc7ac9f..bf278f0c7 100644 --- a/modules/caddyhttp/reverseproxy/hosts.go +++ b/modules/caddyhttp/reverseproxy/hosts.go @@ -17,7 +17,6 @@ package reverseproxy import ( "context" "fmt" - "net/http" "net/netip" "strconv" "sync/atomic" @@ -100,8 +99,7 @@ func (u *Upstream) Full() bool { // fillDialInfo returns a filled DialInfo for upstream u, using the request // context. Note that the returned value is not a pointer. -func (u *Upstream) fillDialInfo(r *http.Request) (DialInfo, error) { - repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) +func (u *Upstream) fillDialInfo(repl *caddy.Replacer) (DialInfo, error) { var addr caddy.NetworkAddress // use provided dial address diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 5198f6908..d80d5776c 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -353,7 +353,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil) } if len(h.NetworkProxyRaw) != 0 { - proxyMod, err := caddyCtx.LoadModule(h, "ForwardProxyRaw") + proxyMod, err := caddyCtx.LoadModule(h, "NetworkProxyRaw") if err != nil { return nil, fmt.Errorf("failed to load network_proxy module: %v", err) } @@ -382,6 +382,36 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e if err != nil { return nil, fmt.Errorf("making TLS client config: %v", err) } + + // servername has a placeholder, so we need to replace it + if strings.Contains(h.TLS.ServerName, "{") { + rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + // reuses the dialer from above to establish a plaintext connection + conn, err := dialContext(ctx, network, addr) + if err != nil { + return nil, err + } + + // but add our own handshake logic + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + tlsConfig := rt.TLSClientConfig.Clone() + tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "") + tlsConn := tls.Client(conn, tlsConfig) + + // complete the handshake before returning the connection + if rt.TLSHandshakeTimeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout) + defer cancel() + } + err = tlsConn.HandshakeContext(ctx) + if err != nil { + _ = tlsConn.Close() + return nil, err + } + return tlsConn, nil + } + } } if h.KeepAlive != nil { @@ -453,45 +483,9 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e return rt, nil } -// replaceTLSServername checks TLS servername to see if it needs replacing -// if it does need replacing, it creates a new cloned HTTPTransport object to avoid any races -// and does the replacing of the TLS servername on that and returns the new object -// if no replacement is necessary it returns the original -func (h *HTTPTransport) replaceTLSServername(repl *caddy.Replacer) *HTTPTransport { - // check whether we have TLS and need to replace the servername in the TLSClientConfig - if h.TLSEnabled() && strings.Contains(h.TLS.ServerName, "{") { - // make a new h, "copy" the parts we don't need to touch, add a new *tls.Config and replace servername - newtransport := &HTTPTransport{ - Resolver: h.Resolver, - TLS: h.TLS, - KeepAlive: h.KeepAlive, - Compression: h.Compression, - MaxConnsPerHost: h.MaxConnsPerHost, - DialTimeout: h.DialTimeout, - FallbackDelay: h.FallbackDelay, - ResponseHeaderTimeout: h.ResponseHeaderTimeout, - ExpectContinueTimeout: h.ExpectContinueTimeout, - MaxResponseHeaderSize: h.MaxResponseHeaderSize, - WriteBufferSize: h.WriteBufferSize, - ReadBufferSize: h.ReadBufferSize, - Versions: h.Versions, - Transport: h.Transport.Clone(), - h2cTransport: h.h2cTransport, - } - newtransport.Transport.TLSClientConfig.ServerName = repl.ReplaceAll(newtransport.Transport.TLSClientConfig.ServerName, "") - return newtransport - } - - return h -} - // RoundTrip implements http.RoundTripper. func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { - // Try to replace TLS servername if needed - repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - transport := h.replaceTLSServername(repl) - - transport.SetScheme(req) + h.SetScheme(req) // use HTTP/3 if enabled (TODO: This is EXPERIMENTAL) if h.h3Transport != nil { @@ -507,7 +501,7 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { return h.h2cTransport.RoundTrip(req) } - return transport.Transport.RoundTrip(req) + return h.Transport.RoundTrip(req) } // SetScheme ensures that the outbound request req @@ -652,7 +646,7 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) return nil, fmt.Errorf("getting tls app: %v", err) } tlsApp := tlsAppIface.(*caddytls.TLS) - err = tlsApp.Manage([]string{t.ClientCertificateAutomate}) + err = tlsApp.Manage(map[string]struct{}{t.ClientCertificateAutomate: {}}) if err != nil { return nil, fmt.Errorf("managing client certificate: %v", err) } diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 395d6e4eb..f6eb58261 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -532,7 +532,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h // the dial address may vary per-request if placeholders are // used, so perform those replacements here; the resulting // DialInfo struct should have valid network address syntax - dialInfo, err := upstream.fillDialInfo(r) + dialInfo, err := upstream.fillDialInfo(repl) if err != nil { return true, fmt.Errorf("making dial info: %v", err) } diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index bf2ebeacc..4830570bf 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -220,7 +220,7 @@ func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssu } if len(iss.NetworkProxyRaw) != 0 { - proxyMod, err := ctx.LoadModule(iss, "ForwardProxyRaw") + proxyMod, err := ctx.LoadModule(iss, "NetworkProxyRaw") if err != nil { return template, fmt.Errorf("failed to load network_proxy module: %v", err) } diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go index 56950bc84..0a9d459df 100644 --- a/modules/caddytls/certmanagers.go +++ b/modules/caddytls/certmanagers.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -143,6 +144,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH qs.Set("server_name", hello.ServerName) qs.Set("signature_schemes", strings.Join(sigs, ",")) qs.Set("cipher_suites", strings.Join(suites, ",")) + localIP, _, err := net.SplitHostPort(hello.Conn.LocalAddr().String()) + if err == nil && localIP != "" { + qs.Set("local_ip", localIP) + } parsed.RawQuery = qs.Encode() req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index fb9588c91..7c8436bc9 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -24,6 +24,7 @@ import ( "fmt" "io" "os" + "reflect" "strings" "github.com/mholt/acmez/v3" @@ -461,6 +462,14 @@ func (p ConnectionPolicy) SettingsEmpty() bool { p.InsecureSecretsLog == "" } +// SettingsEmpty returns true if p's settings (fields +// except the matchers) are the same as q. +func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool { + p.MatchersRaw = nil + q.MatchersRaw = nil + return reflect.DeepEqual(p, q) +} + // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: // // connection_policy { diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index f7b8db995..7329bf1f2 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -138,7 +138,6 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { // all existing configs are now loaded; see if we need to make any new ones // based on the input configuration, and also mark the most recent one(s) as // current/active, so they can be used for ECH retries - for _, cfg := range ech.Configs { publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) @@ -279,7 +278,7 @@ func (t *TLS) publishECHConfigs() error { // if all the (inner) domains have had this ECH config list published // by this publisher, then try the next publication config if len(serverNamesSet) == 0 { - logger.Debug("ECH config list already published by publisher for associated domains", + logger.Debug("ECH config list already published by publisher for associated domains (or no domains to publish for)", zap.Uint8s("config_ids", configIDs), zap.String("publisher", publisherKey)) continue @@ -300,7 +299,7 @@ func (t *TLS) publishECHConfigs() error { err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) if err == nil { t.logger.Info("published ECH configuration list", - zap.Strings("domains", publication.Domains), + zap.Strings("domains", dnsNamesToPublish), zap.Uint8s("config_ids", configIDs), zap.Error(err)) // update publication history, so that we don't unnecessarily republish every time @@ -390,27 +389,33 @@ func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) { return echConfig{}, nil } metaBytes, err := storage.Load(ctx, metaKey) - if err != nil { + if errors.Is(err, fs.ErrNotExist) { + logger.Warn("ECH config metadata file missing; will recreate at next publication", + zap.String("config_id", configID), + zap.Error(err)) + } else if err != nil { delErr := storage.Delete(ctx, cfgIDKey) if delErr != nil { - return echConfig{}, fmt.Errorf("error loading ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + return echConfig{}, fmt.Errorf("error loading ECH config metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) } - logger.Warn("could not load ECH metadata; deleted its config folder", + logger.Warn("could not load ECH config metadata; deleted its folder", zap.String("config_id", configID), zap.Error(err)) return echConfig{}, nil } var meta echConfigMeta - if err := json.Unmarshal(metaBytes, &meta); err != nil { - // even though it's just metadata, reset the whole config since we can't reliably maintain it - delErr := storage.Delete(ctx, cfgIDKey) - if delErr != nil { - return echConfig{}, fmt.Errorf("error decoding ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + if len(metaBytes) > 0 { + if err := json.Unmarshal(metaBytes, &meta); err != nil { + // even though it's just metadata, reset the whole config since we can't reliably maintain it + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error decoding ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not JSON-decode ECH metadata; deleted its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil } - logger.Warn("could not JSON-decode ECH metadata; deleted its config folder", - zap.String("config_id", configID), - zap.Error(err)) - return echConfig{}, nil } cfg.privKeyBin = privKeyBytes @@ -641,10 +646,6 @@ nextName: } relName := libdns.RelativeName(domain+".", zone) - // TODO: libdns.RelativeName should probably return "@" instead of "". (The latest commits of libdns do this, so remove this logic once upgraded.) - if relName == "" { - relName = "@" - } // get existing records for this domain; we need to make sure another // record exists for it so we don't accidentally trample a wildcard; we @@ -657,23 +658,25 @@ nextName: zap.Error(err)) continue } - var httpsRec libdns.Record + var httpsRec libdns.ServiceBinding var nameHasExistingRecord bool for _, rec := range recs { - // TODO: providers SHOULD normalize root-level records to be named "@"; remove the extra conditions when the transition to the new semantics is done - if rec.Name == relName || (rec.Name == "" && relName == "@") { + rr := rec.RR() + if rr.Name == relName { // CNAME records are exclusive of all other records, so we cannot publish an HTTPS // record for a domain that is CNAME'd. See #6922. - if rec.Type == "CNAME" { + if rr.Type == "CNAME" { dnsPub.logger.Warn("domain has CNAME record, so unable to publish ECH data to HTTPS record", zap.String("domain", domain), - zap.String("cname_value", rec.Value)) + zap.String("cname_value", rr.Data)) continue nextName } nameHasExistingRecord = true - if rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { - httpsRec = rec - break + if svcb, ok := rec.(libdns.ServiceBinding); ok && svcb.Scheme == "https" { + if svcb.Target == "" || svcb.Target == "." { + httpsRec = svcb + break + } } } } @@ -689,31 +692,24 @@ nextName: zap.String("zone", zone)) continue } - params := make(svcParams) - if httpsRec.Value != "" { - params, err = parseSvcParams(httpsRec.Value) - if err != nil { - dnsPub.logger.Error("unable to parse existing DNS record to publish ECH data to HTTPS DNS record", - zap.String("domain", domain), - zap.String("https_rec_value", httpsRec.Value), - zap.Error(err)) - continue - } + params := httpsRec.Params + if params == nil { + params = make(libdns.SvcParams) } - // overwrite only the ech SvcParamKey + // overwrite only the "ech" SvcParamKey params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)} // publish record _, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{ - { + libdns.ServiceBinding{ // HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460) - Type: "HTTPS", + Scheme: "https", Name: relName, - Priority: 2, // allows a manual override with priority 1 + TTL: 5 * time.Minute, // TODO: low hard-coded value only temporary; change to a higher value once more field-tested and key rotation is implemented + Priority: 2, // allows a manual override with priority 1 Target: ".", - Value: params.String(), - TTL: 1 * time.Minute, // TODO: for testing only + Params: params, }, }) if err != nil { @@ -952,172 +948,6 @@ func newECHConfigID(ctx caddy.Context) (uint8, error) { return 0, fmt.Errorf("depleted attempts to find an available config_id") } -// svcParams represents SvcParamKey and SvcParamValue pairs as -// described in https://www.rfc-editor.org/rfc/rfc9460 (section 2.1). -type svcParams map[string][]string - -// parseSvcParams parses service parameters into a structured type -// for safer manipulation. -func parseSvcParams(input string) (svcParams, error) { - if len(input) > 4096 { - return nil, fmt.Errorf("input too long: %d", len(input)) - } - - params := make(svcParams) - input = strings.TrimSpace(input) + " " - - for cursor := 0; cursor < len(input); cursor++ { - var key, rawVal string - - keyValPair: - for i := cursor; i < len(input); i++ { - switch input[i] { - case '=': - key = strings.ToLower(strings.TrimSpace(input[cursor:i])) - i++ - cursor = i - - var quoted bool - if input[cursor] == '"' { - quoted = true - i++ - cursor = i - } - - var escaped bool - - for j := cursor; j < len(input); j++ { - switch input[j] { - case '"': - if !quoted { - return nil, fmt.Errorf("illegal DQUOTE at position %d", j) - } - if !escaped { - // end of quoted value - rawVal = input[cursor:j] - j++ - cursor = j - break keyValPair - } - case '\\': - escaped = true - case ' ', '\t', '\n', '\r': - if !quoted { - // end of unquoted value - rawVal = input[cursor:j] - cursor = j - break keyValPair - } - default: - escaped = false - } - } - - case ' ', '\t', '\n', '\r': - // key with no value (flag) - key = input[cursor:i] - params[key] = []string{} - cursor = i - break keyValPair - } - } - - if rawVal == "" { - continue - } - - var sb strings.Builder - - var escape int // start of escape sequence (after \, so 0 is never a valid start) - for i := 0; i < len(rawVal); i++ { - ch := rawVal[i] - if escape > 0 { - // validate escape sequence - // (RFC 9460 Appendix A) - // escaped: "\" ( non-digit / dec-octet ) - // non-digit: "%x21-2F / %x3A-7E" - // dec-octet: "0-255 as a 3-digit decimal number" - if ch >= '0' && ch <= '9' { - // advance to end of decimal octet, which must be 3 digits - i += 2 - if i > len(rawVal) { - return nil, fmt.Errorf("value ends with incomplete escape sequence: %s", rawVal[escape:]) - } - decOctet, err := strconv.Atoi(rawVal[escape : i+1]) - if err != nil { - return nil, err - } - if decOctet < 0 || decOctet > 255 { - return nil, fmt.Errorf("invalid decimal octet in escape sequence: %s (%d)", rawVal[escape:i], decOctet) - } - sb.WriteRune(rune(decOctet)) - escape = 0 - continue - } else if (ch < 0x21 || ch > 0x2F) && (ch < 0x3A && ch > 0x7E) { - return nil, fmt.Errorf("illegal escape sequence %s", rawVal[escape:i]) - } - } - switch ch { - case ';', '(', ')': - // RFC 9460 Appendix A: - // > contiguous = 1*( non-special / escaped ) - // > non-special is VCHAR minus DQUOTE, ";", "(", ")", and "\". - return nil, fmt.Errorf("illegal character in value %q at position %d: %s", rawVal, i, string(ch)) - case '\\': - escape = i + 1 - default: - sb.WriteByte(ch) - escape = 0 - } - } - - params[key] = strings.Split(sb.String(), ",") - } - - return params, nil -} - -// String serializes svcParams into zone presentation format. -func (params svcParams) String() string { - var sb strings.Builder - for key, vals := range params { - if sb.Len() > 0 { - sb.WriteRune(' ') - } - sb.WriteString(key) - var hasVal, needsQuotes bool - for _, val := range vals { - if len(val) > 0 { - hasVal = true - } - if strings.ContainsAny(val, `" `) { - needsQuotes = true - } - if hasVal && needsQuotes { - break - } - } - if hasVal { - sb.WriteRune('=') - } - if needsQuotes { - sb.WriteRune('"') - } - for i, val := range vals { - if i > 0 { - sb.WriteRune(',') - } - val = strings.ReplaceAll(val, `"`, `\"`) - val = strings.ReplaceAll(val, `,`, `\,`) - sb.WriteString(val) - } - if needsQuotes { - sb.WriteRune('"') - } - } - return sb.String() -} - // ECHPublisher is an interface for publishing ECHConfigList values // so that they can be used by clients. type ECHPublisher interface { diff --git a/modules/caddytls/ech_test.go b/modules/caddytls/ech_test.go deleted file mode 100644 index 3e566f5a7..000000000 --- a/modules/caddytls/ech_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package caddytls - -import ( - "reflect" - "testing" -) - -func TestParseSvcParams(t *testing.T) { - for i, test := range []struct { - input string - expect svcParams - shouldErr bool - }{ - { - input: `alpn="h2,h3" no-default-alpn ipv6hint=2001:db8::1 port=443`, - expect: svcParams{ - "alpn": {"h2", "h3"}, - "no-default-alpn": {}, - "ipv6hint": {"2001:db8::1"}, - "port": {"443"}, - }, - }, - { - input: `key=value quoted="some string" flag`, - expect: svcParams{ - "key": {"value"}, - "quoted": {"some string"}, - "flag": {}, - }, - }, - { - input: `key="nested \"quoted\" value,foobar"`, - expect: svcParams{ - "key": {`nested "quoted" value`, "foobar"}, - }, - }, - { - input: `alpn=h3,h2 tls-supported-groups=29,23 no-default-alpn ech="foobar"`, - expect: svcParams{ - "alpn": {"h3", "h2"}, - "tls-supported-groups": {"29", "23"}, - "no-default-alpn": {}, - "ech": {"foobar"}, - }, - }, - { - input: `escape=\097`, - expect: svcParams{ - "escape": {"a"}, - }, - }, - { - input: `escapes=\097\098c`, - expect: svcParams{ - "escapes": {"abc"}, - }, - }, - } { - actual, err := parseSvcParams(test.input) - if err != nil && !test.shouldErr { - t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test.input) - continue - } else if err == nil && test.shouldErr { - t.Errorf("Test %d: Expected an error, but got no error (input=%q)", i, test.input) - continue - } - if !reflect.DeepEqual(test.expect, actual) { - t.Errorf("Test %d: Expected %v, got %v (input=%q)", i, test.expect, actual, test.input) - continue - } - } -} - -func TestSvcParamsString(t *testing.T) { - // this test relies on the parser also working - // because we can't just compare string outputs - // since map iteration is unordered - for i, test := range []svcParams{ - { - "alpn": {"h2", "h3"}, - "no-default-alpn": {}, - "ipv6hint": {"2001:db8::1"}, - "port": {"443"}, - }, - - { - "key": {"value"}, - "quoted": {"some string"}, - "flag": {}, - }, - { - "key": {`nested "quoted" value`, "foobar"}, - }, - { - "alpn": {"h3", "h2"}, - "tls-supported-groups": {"29", "23"}, - "no-default-alpn": {}, - "ech": {"foobar"}, - }, - } { - combined := test.String() - parsed, err := parseSvcParams(combined) - if err != nil { - t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test) - continue - } - if len(parsed) != len(test) { - t.Errorf("Test %d: Expected %d keys, but got %d", i, len(test), len(parsed)) - continue - } - for key, expectedVals := range test { - if expected, actual := len(expectedVals), len(parsed[key]); expected != actual { - t.Errorf("Test %d: Expected key %s to have %d values, but had %d", i, key, expected, actual) - continue - } - for j, expected := range expectedVals { - if actual := parsed[key][j]; actual != expected { - t.Errorf("Test %d key %q value %d: Expected '%s' but got '%s'", i, key, j, expected, actual) - continue - } - } - } - if !reflect.DeepEqual(parsed, test) { - t.Errorf("Test %d: Expected %#v, got %#v", i, test, combined) - continue - } - } -} diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 0f8433960..7b49c0208 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -33,6 +33,7 @@ import ( "go.uber.org/zap/zapcore" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/internal" "github.com/caddyserver/caddy/v2/modules/caddyevents" ) @@ -55,8 +56,10 @@ type TLS struct { // // The "automate" certificate loader module can be used to // specify a list of subjects that need certificates to be - // managed automatically. The first matching automation - // policy will be applied to manage the certificate(s). + // managed automatically, including subdomains that may + // already be covered by a managed wildcard certificate. + // The first matching automation policy will be used + // to manage automated certificate(s). // // All loaded certificates get pooled // into the same cache and may be used to complete TLS @@ -123,7 +126,7 @@ type TLS struct { dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) certificateLoaders []CertificateLoader - automateNames []string + automateNames map[string]struct{} ctx caddy.Context storageCleanTicker *time.Ticker storageCleanStop chan struct{} @@ -218,12 +221,13 @@ func (t *TLS) Provision(ctx caddy.Context) error { // special case; these will be loaded in later using our automation facilities, // which we want to avoid doing during provisioning if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil { - repl := caddy.NewReplacer() - subjects := make([]string, len(*automateNames)) - for i, sub := range *automateNames { - subjects[i] = repl.ReplaceAll(sub, "") + if t.automateNames == nil { + t.automateNames = make(map[string]struct{}) + } + repl := caddy.NewReplacer() + for _, sub := range *automateNames { + t.automateNames[repl.ReplaceAll(sub, "")] = struct{}{} } - t.automateNames = append(t.automateNames, subjects...) } else { return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) } @@ -283,7 +287,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("provisioning default public automation policy: %v", err) } - for _, n := range t.automateNames { + for n := range t.automateNames { // if any names specified by the "automate" loader do not qualify for a public // certificate, we should initialize a default internal automation policy // (but we don't want to do this unnecessarily, since it may prompt for password!) @@ -339,8 +343,14 @@ func (t *TLS) Provision(ctx caddy.Context) error { // outer names should have certificates to reduce client brittleness for _, outerName := range outerNames { + if outerName == "" { + continue + } if !t.HasCertificateForSubject(outerName) { - t.automateNames = append(t.automateNames, outerNames...) + if t.automateNames == nil { + t.automateNames = make(map[string]struct{}) + } + t.automateNames[outerName] = struct{}{} } } } @@ -449,7 +459,8 @@ func (t *TLS) Cleanup() error { // app instance (which is being stopped) that are not managed or loaded by the // new app instance (which just started), and remove them from the cache var noLongerManaged []certmagic.SubjectIssuer - var reManage, noLongerLoaded []string + var noLongerLoaded []string + reManage := make(map[string]struct{}) for subj, currentIssuerKey := range t.managing { // It's a bit nuanced: managed certs can sometimes be different enough that we have to // swap them out for a different one, even if they are for the same subject/domain. @@ -467,7 +478,7 @@ func (t *TLS) Cleanup() error { // then, if the next app is managing a cert for this name, but with a different issuer, re-manage it if ok && nextIssuerKey != currentIssuerKey { - reManage = append(reManage, subj) + reManage[subj] = struct{}{} } } } @@ -488,7 +499,7 @@ func (t *TLS) Cleanup() error { if err := nextTLSApp.Manage(reManage); err != nil { if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil { c.Write( - zap.Strings("subjects", reManage), + zap.Strings("subjects", internal.MaxSizeSubjectsListForLog(reManage, 1000)), zap.Error(err), ) } @@ -509,17 +520,31 @@ func (t *TLS) Cleanup() error { return nil } -// Manage immediately begins managing names according to the -// matching automation policy. -func (t *TLS) Manage(names []string) error { +// Manage immediately begins managing subjects according to the +// matching automation policy. The subjects are given in a map +// to prevent duplication and also because quick lookups are +// needed to assess wildcard coverage, if any, depending on +// certain config parameters (with lots of subjects, computing +// wildcard coverage over a slice can be highly inefficient). +func (t *TLS) Manage(subjects map[string]struct{}) error { // for a large number of names, we can be more memory-efficient // by making only one certmagic.Config for all the names that // use that config, rather than calling ManageAsync once for // every name; so first, bin names by AutomationPolicy policyToNames := make(map[*AutomationPolicy][]string) - for _, name := range names { - ap := t.getAutomationPolicyForName(name) - policyToNames[ap] = append(policyToNames[ap], name) + for subj := range subjects { + ap := t.getAutomationPolicyForName(subj) + // by default, if a wildcard that covers the subj is also being + // managed, either by a previous call to Manage or by this one, + // prefer using that over individual certs for its subdomains; + // but users can disable this and force getting a certificate for + // subdomains by adding the name to the 'automate' cert loader + if t.managingWildcardFor(subj, subjects) { + if _, ok := t.automateNames[subj]; !ok { + continue + } + } + policyToNames[ap] = append(policyToNames[ap], subj) } // now that names are grouped by policy, we can simply make one @@ -530,7 +555,7 @@ func (t *TLS) Manage(names []string) error { if err != nil { const maxNamesToDisplay = 100 if len(names) > maxNamesToDisplay { - names = append(names[:maxNamesToDisplay], fmt.Sprintf("(%d more...)", len(names)-maxNamesToDisplay)) + names = append(names[:maxNamesToDisplay], fmt.Sprintf("(and %d more...)", len(names)-maxNamesToDisplay)) } return fmt.Errorf("automate: manage %v: %v", names, err) } @@ -555,6 +580,43 @@ func (t *TLS) Manage(names []string) error { return nil } +// managingWildcardFor returns true if the app is managing a certificate that covers that +// subject name (including consideration of wildcards), either from its internal list of +// names that it IS managing certs for, or from the otherSubjsToManage which includes names +// that WILL be managed. +func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool { + // TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(), + // but that does not account for how manually-loaded certs may be restricted as to which + // hostnames or ClientHellos they can be used with by tags, etc; I don't *think* anyone + // necessarily wants this anyway, but I thought I'd note this here for now (if we did + // consider manually-loaded certs, we'd probably want to rename the method since it + // wouldn't be just about managed certs anymore) + + // IP addresses must match exactly + if ip := net.ParseIP(subj); ip != nil { + _, managing := t.managing[subj] + return managing + } + + // replace labels of the domain with wildcards until we get a match + labels := strings.Split(subj, ".") + for i := range labels { + if labels[i] == "*" { + continue + } + labels[i] = "*" + candidate := strings.Join(labels, ".") + if _, ok := t.managing[candidate]; ok { + return true + } + if _, ok := otherSubjsToManage[candidate]; ok { + return true + } + } + + 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