From 50778b55425d378f709599c0d424b0138af592f4 Mon Sep 17 00:00:00 2001 From: Hyeonggeun Oh Date: Tue, 7 Jan 2025 16:21:57 -0800 Subject: [PATCH 01/73] fix: disable h3 for unix domain socket (#6769) --- modules/caddyhttp/app.go | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 850d3aa8f..5477ed8fe 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -529,21 +529,6 @@ func (app *App) Start() error { // enable TLS if there is a policy and if this is not the HTTP port useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() - // enable HTTP/3 if configured - if h3ok && useTLS { - app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) - if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil { - return err - } - } - - if h3ok && !useTLS { - // Can only serve h3 with TLS enabled - app.logger.Warn("HTTP/3 skipped because it requires TLS", - zap.String("network", listenAddr.Network), - zap.String("addr", hostport)) - } - if h1ok || h2ok && useTLS || h2cok { // create the listener for this socket lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)}) @@ -614,6 +599,33 @@ func (app *App) Start() error { zap.String("network", listenAddr.Network), zap.String("addr", hostport)) } + + if h3ok { + // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses + // a different transport mechanism... which is fine, but the OS doesn't + // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they + // are still one file on the system. So even though "unixpacket" and + // "unixgram" are different network types just as "tcp" and "udp" are, + // the OS will not let us use the same file as both STREAM and DGRAM. + if listenAddr.IsUnixNetwork() { + app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket", + zap.String("file", hostport)) + continue + } + + if useTLS { + // enable HTTP/3 if configured + app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport)) + if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil { + return err + } + } else { + // Can only serve h3 with TLS enabled + app.logger.Warn("HTTP/3 skipped because it requires TLS", + zap.String("network", listenAddr.Network), + zap.String("addr", hostport)) + } + } } } From 1f927d6b07d52d7cf46f1f3020c1ea5993a3e5e8 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Tue, 7 Jan 2025 21:51:03 -0700 Subject: [PATCH 02/73] log: Only chmod if permission bits differ; make log dir (#6761) * log: Only chmod if permission bits differ Follow-up to #6314 and https://caddy.community/t/caddy-2-9-0-breaking-change/27576/11 * Fix test * Refactor FileWriter * Ooooh octal... right... --- modules/logging/filewriter.go | 92 ++++++++++++++++++------------ modules/logging/filewriter_test.go | 5 +- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index 62d500dca..ef3211cbb 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -20,6 +20,7 @@ import ( "io" "math" "os" + "path/filepath" "strconv" "github.com/dustin/go-humanize" @@ -146,51 +147,68 @@ func (fw FileWriter) WriterKey() string { // OpenWriter opens a new file writer. func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { - if fw.Mode == 0 { - fw.Mode = 0o600 + modeIfCreating := os.FileMode(fw.Mode) + if modeIfCreating == 0 { + modeIfCreating = 0o600 } - // roll log files by default - if fw.Roll == nil || *fw.Roll { - if fw.RollSizeMB == 0 { - fw.RollSizeMB = 100 - } - if fw.RollCompress == nil { - compress := true - fw.RollCompress = &compress - } - if fw.RollKeep == 0 { - fw.RollKeep = 10 - } - if fw.RollKeepDays == 0 { - fw.RollKeepDays = 90 - } + // roll log files as a sensible default to avoid disk space exhaustion + roll := fw.Roll == nil || *fw.Roll - // create the file if it does not exist with the right mode. - // lumberjack will reuse the file mode across log rotation. - f_tmp, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode)) + // create the file if it does not exist; create with the configured mode, or default + // to restrictive if not set. (lumberjack will reuse the file mode across log rotation) + if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil { + return nil, err + } + file, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, modeIfCreating) + if err != nil { + return nil, err + } + info, err := file.Stat() + if roll { + file.Close() // lumberjack will reopen it on its own + } + + // Ensure already existing files have the right mode, since OpenFile will not set the mode in such case. + if configuredMode := os.FileMode(fw.Mode); configuredMode != 0 { if err != nil { - return nil, err + return nil, fmt.Errorf("unable to stat log file to see if we need to set permissions: %v", err) } - f_tmp.Close() - // ensure already existing files have the right mode, - // since OpenFile will not set the mode in such case. - if err = os.Chmod(fw.Filename, os.FileMode(fw.Mode)); err != nil { - return nil, err + // only chmod if the configured mode is different + if info.Mode()&os.ModePerm != configuredMode&os.ModePerm { + if err = os.Chmod(fw.Filename, configuredMode); err != nil { + return nil, err + } } - - return &lumberjack.Logger{ - Filename: fw.Filename, - MaxSize: fw.RollSizeMB, - MaxAge: fw.RollKeepDays, - MaxBackups: fw.RollKeep, - LocalTime: fw.RollLocalTime, - Compress: *fw.RollCompress, - }, nil } - // otherwise just open a regular file - return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode)) + // if not rolling, then the plain file handle is all we need + if !roll { + return file, nil + } + + // otherwise, return a rolling log + if fw.RollSizeMB == 0 { + fw.RollSizeMB = 100 + } + if fw.RollCompress == nil { + compress := true + fw.RollCompress = &compress + } + if fw.RollKeep == 0 { + fw.RollKeep = 10 + } + if fw.RollKeepDays == 0 { + fw.RollKeepDays = 90 + } + return &lumberjack.Logger{ + Filename: fw.Filename, + MaxSize: fw.RollSizeMB, + MaxAge: fw.RollKeepDays, + MaxBackups: fw.RollKeep, + LocalTime: fw.RollLocalTime, + Compress: *fw.RollCompress, + }, nil } // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: diff --git a/modules/logging/filewriter_test.go b/modules/logging/filewriter_test.go index 0c54a6590..f9072f98a 100644 --- a/modules/logging/filewriter_test.go +++ b/modules/logging/filewriter_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "os" "path" + "path/filepath" "syscall" "testing" @@ -77,7 +78,7 @@ func TestFileCreationMode(t *testing.T) { t.Fatalf("failed to create tempdir: %v", err) } defer os.RemoveAll(dir) - fpath := path.Join(dir, "test.log") + fpath := filepath.Join(dir, "test.log") tt.fw.Filename = fpath logger, err := tt.fw.OpenWriter() @@ -92,7 +93,7 @@ func TestFileCreationMode(t *testing.T) { } if st.Mode() != tt.wantMode { - t.Errorf("file mode is %v, want %v", st.Mode(), tt.wantMode) + t.Errorf("%s: file mode is %v, want %v", tt.name, st.Mode(), tt.wantMode) } }) } From e48b75843b7eff2948b573391fb41535b5e333ef Mon Sep 17 00:00:00 2001 From: Arsh <69170106+lilnasy@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:18:06 +0530 Subject: [PATCH 03/73] header: `match` subdirective for response matching (#6765) --- .../caddyfile_adapt/header.caddyfiletest | 14 ++++++++++++ modules/caddyhttp/headers/caddyfile.go | 10 +++++++++ modules/caddyhttp/headers/headers_test.go | 22 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/caddytest/integration/caddyfile_adapt/header.caddyfiletest b/caddytest/integration/caddyfile_adapt/header.caddyfiletest index ec2a842a3..a0af24ff6 100644 --- a/caddytest/integration/caddyfile_adapt/header.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/header.caddyfiletest @@ -12,10 +12,14 @@ @images path /images/* header @images { Cache-Control "public, max-age=3600, stale-while-revalidate=86400" + match { + status 200 + } } header { +Link "Foo" +Link "Bar" + match status 200 } header >Set Defer header >Replace Deferred Replacement @@ -42,6 +46,11 @@ { "handler": "headers", "response": { + "require": { + "status_code": [ + 200 + ] + }, "set": { "Cache-Control": [ "public, max-age=3600, stale-while-revalidate=86400" @@ -136,6 +145,11 @@ "Foo", "Bar" ] + }, + "require": { + "status_code": [ + 200 + ] } } }, diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go index e55e9fab2..f060471b1 100644 --- a/modules/caddyhttp/headers/caddyfile.go +++ b/modules/caddyhttp/headers/caddyfile.go @@ -99,6 +99,16 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) handler.Response.Deferred = true continue } + if field == "match" { + responseMatchers := make(map[string]caddyhttp.ResponseMatcher) + err := caddyhttp.ParseNamedResponseMatcher(h.NewFromNextSegment(), responseMatchers) + if err != nil { + return nil, err + } + matcher := responseMatchers["match"] + handler.Response.Require = &matcher + continue + } if hasArgs { return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird } diff --git a/modules/caddyhttp/headers/headers_test.go b/modules/caddyhttp/headers/headers_test.go index d74e6fc3a..9808c29c9 100644 --- a/modules/caddyhttp/headers/headers_test.go +++ b/modules/caddyhttp/headers/headers_test.go @@ -143,6 +143,28 @@ func TestHandler(t *testing.T) { "Cache-Control": []string{"no-cache"}, }, }, + { // same as above, but checks that response headers are left alone when "Require" conditions are unmet + handler: Handler{ + Response: &RespHeaderOps{ + Require: &caddyhttp.ResponseMatcher{ + Headers: http.Header{ + "Cache-Control": nil, + }, + }, + HeaderOps: &HeaderOps{ + Add: http.Header{ + "Cache-Control": []string{"no-cache"}, + }, + }, + }, + }, + respHeader: http.Header{ + "Cache-Control": []string{"something"}, + }, + expectedRespHeader: http.Header{ + "Cache-Control": []string{"something"}, + }, + }, { handler: Handler{ Response: &RespHeaderOps{ From 0e570e0cc717f02cf3800ae741df70cd074c7275 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 8 Jan 2025 07:43:27 -0700 Subject: [PATCH 04/73] go.mod: UPgrade CertMagic to 0.21.6 (fix ARI handshake maintenance) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 67443562f..d36925d81 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.14.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.21.5 + github.com/caddyserver/certmagic v0.21.6 github.com/caddyserver/zerossl v0.1.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 diff --git a/go.sum b/go.sum index 538304a28..169666412 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,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.21.5 h1:iIga4nZRgd27EIEbX7RZmoRMul+EVBn/h7bAGL83dnY= -github.com/caddyserver/certmagic v0.21.5/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw= +github.com/caddyserver/certmagic v0.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZhAHlZFPE= +github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw= 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= From 1f35a8a4029a338e89998acafa95e1e931a46a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 9 Jan 2025 19:54:44 +0100 Subject: [PATCH 05/73] fastcgi: improve parsePHPFastCGI docs (#6779) --- modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 6fe7df3fd..5db73a4a2 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -131,15 +131,18 @@ func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // is equivalent to a route consisting of: // // # Add trailing slash for directory requests +// # This redirection is automatically disabled if "{http.request.uri.path}/index.php" +// # doesn't appear in the try_files list // @canonicalPath { // file {path}/index.php // not path */ // } // redir @canonicalPath {path}/ 308 // -// # If the requested file does not exist, try index files +// # If the requested file does not exist, try index files and assume index.php always exists // @indexFiles file { // try_files {path} {path}/index.php index.php +// try_policy first_exist_fallback // split_path .php // } // rewrite @indexFiles {http.matchers.file.relative} From 2c4295ee48f494bc8dda5fa09b37612d520c8b3b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 9 Jan 2025 13:57:00 -0700 Subject: [PATCH 06/73] caddytls: Initial support for ACME profiles Still very experimental; only deployed to LE staging so far. --- go.mod | 4 ++-- go.sum | 8 ++++---- modules/caddytls/acmeissuer.go | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index d36925d81..495d893b1 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.14.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.21.6 + github.com/caddyserver/certmagic v0.21.7-0.20250109205135-32654015b016 github.com/caddyserver/zerossl v0.1.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 @@ -17,7 +17,7 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.17.11 github.com/klauspost/cpuid/v2 v2.2.9 - github.com/mholt/acmez/v3 v3.0.0 + github.com/mholt/acmez/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 github.com/quic-go/quic-go v0.48.2 github.com/smallstep/certificates v0.26.1 diff --git a/go.sum b/go.sum index 169666412..c2179ad87 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,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.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZhAHlZFPE= -github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw= +github.com/caddyserver/certmagic v0.21.7-0.20250109205135-32654015b016 h1:bwnFMkCXIgw3WO7vvMwpr7Zf8qfADmMzYe6mxSKC7zI= +github.com/caddyserver/certmagic v0.21.7-0.20250109205135-32654015b016/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= 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= @@ -344,8 +344,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.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E= -github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/acmez/v3 v3.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8= +github.com/mholt/acmez/v3 v3.0.1/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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 29a5954e7..2fe5eec97 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -60,6 +60,14 @@ type ACMEIssuer struct { // other than ACME transactions. Email string `json:"email,omitempty"` + // Optionally select an ACME profile to use for certificate + // orders. Must be a profile name offered by the ACME server, + // which are listed at its directory endpoint. + // + // EXPERIMENTAL: Subject to change. + // See https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + Profile string `json:"profile,omitempty"` + // If you have an existing account with the ACME server, put // the private key here in PEM format. The ACME client will // look up your account information with this key first before @@ -184,6 +192,7 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { CA: iss.CA, TestCA: iss.TestCA, Email: iss.Email, + Profile: iss.Profile, AccountKeyPEM: iss.AccountKey, CertObtainTimeout: time.Duration(iss.ACMETimeout), TrustedRoots: iss.rootPool, @@ -338,6 +347,7 @@ func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct a // dir // test_dir // email +// profile // timeout // disable_http_challenge // disable_tlsalpn_challenge @@ -400,6 +410,11 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } + case "profile": + if !d.AllArgs(&iss.Profile) { + return d.ArgErr() + } + case "timeout": var timeoutStr string if !d.AllArgs(&timeoutStr) { From 9e0e5a4b4c2babda81c58f28fe61adfa91d04524 Mon Sep 17 00:00:00 2001 From: Omar Ramadan Date: Thu, 16 Jan 2025 09:06:29 -0800 Subject: [PATCH 07/73] logging: Fix crash if logging error is not HandlerError (#6777) Co-authored-by: Matt Holt --- modules/caddyhttp/logging.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 0a389fe16..87298ac3c 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -211,6 +211,11 @@ func errLogValues(err error) (status int, msg string, fields func() []zapcore.Fi } return } + fields = func() []zapcore.Field { + return []zapcore.Field{ + zap.Error(err), + } + } status = http.StatusInternalServerError msg = err.Error() return From e7da3b267bcec986aaca960dd22ef834d3b9d4a6 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 17 Jan 2025 06:49:01 -0700 Subject: [PATCH 08/73] reverseproxy: Via header (#6275) --- .../caddyhttp/reverseproxy/reverseproxy.go | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 230bec951..a15dbe424 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -683,7 +683,7 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. req.Header.Set("Early-Data", "1") } - reqUpType := upgradeType(req.Header) + reqUpgradeType := upgradeType(req.Header) removeConnectionHeaders(req.Header) // Remove hop-by-hop headers to the backend. Especially @@ -704,9 +704,9 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. // After stripping all the hop-by-hop connection headers above, add back any // necessary for protocol upgrades, such as for websockets. - if reqUpType != "" { + if reqUpgradeType != "" { req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", reqUpType) + req.Header.Set("Upgrade", reqUpgradeType) normalizeWebsocketHeaders(req.Header) } @@ -732,6 +732,9 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. return nil, err } + // Via header(s) + req.Header.Add("Via", fmt.Sprintf("%d.%d Caddy", req.ProtoMajor, req.ProtoMinor)) + return req, nil } @@ -882,13 +885,15 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe }), ) + const logMessage = "upstream roundtrip" + if err != nil { - if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil { + if c := logger.Check(zapcore.DebugLevel, logMessage); c != nil { c.Write(zap.Error(err)) } return err } - if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil { + if c := logger.Check(zapcore.DebugLevel, logMessage); c != nil { c.Write( zap.Object("headers", caddyhttp.LoggableHTTPHeader{ Header: res.Header, @@ -1024,6 +1029,14 @@ func (h *Handler) finalizeResponse( res.Header.Del(h) } + // delete our Server header and use Via instead (see #6275) + rw.Header().Del("Server") + var protoPrefix string + if !strings.HasPrefix(strings.ToUpper(res.Proto), "HTTP/") { + protoPrefix = res.Proto[:strings.Index(res.Proto, "/")+1] + } + rw.Header().Add("Via", fmt.Sprintf("%s%d.%d Caddy", protoPrefix, res.ProtoMajor, res.ProtoMinor)) + // apply any response header operations if h.Headers != nil && h.Headers.Response != nil { if h.Headers.Response.Require == nil || From 99073eaa33af62bff51c31305e3437c57d936284 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 17 Jan 2025 06:54:58 -0700 Subject: [PATCH 09/73] go.mod: Upgrade CertMagic to v0.21.7 Fixes rare edge case panics related to ARI --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 495d893b1..6bd356bbd 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.14.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.21.7-0.20250109205135-32654015b016 + github.com/caddyserver/certmagic v0.21.7 github.com/caddyserver/zerossl v0.1.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 diff --git a/go.sum b/go.sum index c2179ad87..71867a8f8 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,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.21.7-0.20250109205135-32654015b016 h1:bwnFMkCXIgw3WO7vvMwpr7Zf8qfADmMzYe6mxSKC7zI= -github.com/caddyserver/certmagic v0.21.7-0.20250109205135-32654015b016/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= +github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= +github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= 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= From 8d748bee711e6678510b8387140e07f5f4b2976f Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Fri, 24 Jan 2025 05:07:19 +0100 Subject: [PATCH 10/73] chore: update quic-go to v0.49.0 (#6803) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6bd356bbd..35f06bcd8 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 github.com/mholt/acmez/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.48.2 + github.com/quic-go/quic-go v0.49.0 github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 @@ -74,7 +74,7 @@ require ( go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect - go.uber.org/mock v0.4.0 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect diff --git a/go.sum b/go.sum index 71867a8f8..b5622ba51 100644 --- a/go.sum +++ b/go.sum @@ -392,8 +392,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.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= -github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= +github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= 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= @@ -564,8 +564,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= -go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= From 30743c361ad2277a3bffe67c436805e2b224fbe7 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 25 Jan 2025 02:37:16 +0300 Subject: [PATCH 11/73] chore: don't use deprecated `archives.format_overrides.format` (#6807) Signed-off-by: Mohammed Al Sahaf --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 005fdbaf3..c7ed4b365 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -111,7 +111,7 @@ archives: - id: default format_overrides: - goos: windows - format: zip + formats: zip name_template: >- {{ .ProjectName }}_ {{- .Version }}_ From 7b8f3505e33139de0d542566478e98b361bb84bf Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:45:41 +0300 Subject: [PATCH 12/73] caddytls: Fix sni_regexp matcher to obtain layer4 contexts (#6804) * caddytls: Fix sni_regexp matcher * caddytls: Refactor sni_regexp matcher --- modules/caddytls/matchers.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index a74d6ea80..fec1ffd8e 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -15,6 +15,7 @@ package caddytls import ( + "context" "crypto/tls" "fmt" "net" @@ -224,15 +225,28 @@ func (MatchServerNameRE) CaddyModule() caddy.ModuleInfo { // Match matches hello based on SNI using a regular expression. func (m MatchServerNameRE) Match(hello *tls.ClientHelloInfo) bool { - repl := caddy.NewReplacer() - // caddytls.TestServerNameMatcher calls this function without any context - if ctx := hello.Context(); ctx != nil { + // Note: caddytls.TestServerNameMatcher calls this function without any context + ctx := hello.Context() + if ctx == nil { + // layer4.Connection implements GetContext() to pass its context here, + // since hello.Context() returns nil + if mayHaveContext, ok := hello.Conn.(interface{ GetContext() context.Context }); ok { + ctx = mayHaveContext.GetContext() + } + } + + var repl *caddy.Replacer + if ctx != nil { // In some situations the existing context may have no replacer if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil { repl = replAny.(*caddy.Replacer) } } + if repl == nil { + repl = caddy.NewReplacer() + } + return m.MatchRegexp.Match(hello.ServerName, repl) } From 11151586165946453275b66ef33794d41a5e047b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 27 Jan 2025 08:18:34 -0700 Subject: [PATCH 13/73] caddyhttp: ResponseRecorder sets stream regardless of 1xx Fixes a panic where rr.stream is not true when it should be in the event of 1xx, because the buf is nil --- modules/caddyhttp/responsewriter.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/caddyhttp/responsewriter.go b/modules/caddyhttp/responsewriter.go index 3c0f89d0b..904c30c03 100644 --- a/modules/caddyhttp/responsewriter.go +++ b/modules/caddyhttp/responsewriter.go @@ -154,16 +154,16 @@ func (rr *responseRecorder) WriteHeader(statusCode int) { // connections by manually setting headers and writing status 101 rr.statusCode = statusCode + // decide whether we should buffer the response + if rr.shouldBuffer == nil { + rr.stream = true + } else { + rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) + } + // 1xx responses aren't final; just informational if statusCode < 100 || statusCode > 199 { rr.wroteHeader = true - - // decide whether we should buffer the response - if rr.shouldBuffer == nil { - rr.stream = true - } else { - rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header()) - } } // if informational or not buffered, immediately write header From 066d770409917b409d0bdc14cb5ba33d3e4cb33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 27 Jan 2025 17:32:24 +0100 Subject: [PATCH 14/73] cmd: automatically set GOMEMLIMIT (#6809) * feat: automatically set GOMEMLIMIT * add system support * comments * add logs --- cmd/main.go | 23 ++++++++++++++++++++++- go.mod | 2 ++ go.sum | 4 ++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index 655c0084b..c41738be0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,6 +24,7 @@ import ( "io" "io/fs" "log" + "log/slog" "net" "os" "path/filepath" @@ -33,10 +34,12 @@ import ( "strings" "time" + "github.com/KimMachineGun/automemlimit/memlimit" "github.com/caddyserver/certmagic" "github.com/spf13/pflag" "go.uber.org/automaxprocs/maxprocs" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -66,12 +69,30 @@ func Main() { os.Exit(caddy.ExitCodeFailedStartup) } - undo, err := maxprocs.Set() + logger := caddy.Log() + + // Configure the maximum number of CPUs to use to match the Linux container quota (if any) + // See https://pkg.go.dev/runtime#GOMAXPROCS + undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof)) defer undo() if err != nil { caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) } + // Configure the maximum memory to use to match the Linux container quota (if any) or system memory + // See https://pkg.go.dev/runtime/debug#SetMemoryLimit + _, _ = memlimit.SetGoMemLimitWithOpts( + memlimit.WithLogger( + slog.New(zapslog.NewHandler(logger.Core())), + ), + memlimit.WithProvider( + memlimit.ApplyFallback( + memlimit.FromCgroup, + memlimit.FromSystem, + ), + ), + ) + if err := defaultFactory.Build().Execute(); err != nil { var exitError *exitError if errors.As(err, &exitError) { diff --git a/go.mod b/go.mod index 35f06bcd8..4d9f5ff36 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect + github.com/KimMachineGun/automemlimit v0.7.0 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -63,6 +64,7 @@ require ( github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/onsi/ginkgo/v2 v2.13.2 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect diff --git a/go.sum b/go.sum index b5622ba51..909c7e0b9 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KimMachineGun/automemlimit v0.7.0 h1:7G06p/dMSf7G8E6oq+f2uOPuVncFyIlDI/pBWK49u88= +github.com/KimMachineGun/automemlimit v0.7.0/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -366,6 +368,8 @@ github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= From d7872c3bfa673ce9584d00f01a725b93fa7bedf1 Mon Sep 17 00:00:00 2001 From: vnxme <46669194+vnxme@users.noreply.github.com> Date: Mon, 27 Jan 2025 21:42:09 +0300 Subject: [PATCH 15/73] caddytls: Refactor sni matcher (#6812) --- modules/caddytls/matchers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index fec1ffd8e..dfbec94cc 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -56,7 +56,7 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo { // Match matches hello based on SNI. func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { - repl := caddy.NewReplacer() + var repl *caddy.Replacer // caddytls.TestServerNameMatcher calls this function without any context if ctx := hello.Context(); ctx != nil { // In some situations the existing context may have no replacer @@ -65,6 +65,10 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool { } } + if repl == nil { + repl = caddy.NewReplacer() + } + for _, name := range m { rs := repl.ReplaceAll(name, "") if certmagic.MatchWildcard(hello.ServerName, rs) { From 904a0fa368b7eacac3c7156ce4a1f6ced8f61f34 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Tue, 28 Jan 2025 00:30:54 +0300 Subject: [PATCH 16/73] reverse_proxy: re-add healthy upstreams metric (#6806) * reverse_proxy: re-add healthy upstreams metric Signed-off-by: Mohammed Al Sahaf * lint Signed-off-by: Mohammed Al Sahaf --------- Signed-off-by: Mohammed Al Sahaf --- modules/caddyhttp/reverseproxy/metrics.go | 20 ++++++++++--------- .../caddyhttp/reverseproxy/reverseproxy.go | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/metrics.go b/modules/caddyhttp/reverseproxy/metrics.go index f744756d3..8a7105c11 100644 --- a/modules/caddyhttp/reverseproxy/metrics.go +++ b/modules/caddyhttp/reverseproxy/metrics.go @@ -2,26 +2,26 @@ package reverseproxy import ( "runtime/debug" - "sync" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/caddyserver/caddy/v2" ) var reverseProxyMetrics = struct { - init sync.Once upstreamsHealthy *prometheus.GaugeVec logger *zap.Logger }{} -func initReverseProxyMetrics(handler *Handler) { +func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) { const ns, sub = "caddy", "reverse_proxy" upstreamsLabels := []string{"upstream"} - reverseProxyMetrics.upstreamsHealthy = promauto.NewGaugeVec(prometheus.GaugeOpts{ + reverseProxyMetrics.upstreamsHealthy = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{ Namespace: ns, Subsystem: sub, Name: "upstreams_healthy", @@ -35,17 +35,19 @@ type metricsUpstreamsHealthyUpdater struct { handler *Handler } -func newMetricsUpstreamsHealthyUpdater(handler *Handler) *metricsUpstreamsHealthyUpdater { - reverseProxyMetrics.init.Do(func() { - initReverseProxyMetrics(handler) - }) +const upstreamsHealthyMetrics caddy.CtxKey = "reverse_proxy_upstreams_healthy" +func newMetricsUpstreamsHealthyUpdater(handler *Handler, ctx caddy.Context) *metricsUpstreamsHealthyUpdater { + if set := ctx.Value(upstreamsHealthyMetrics); set == nil { + initReverseProxyMetrics(handler, ctx.GetMetricsRegistry()) + ctx = ctx.WithValue(upstreamsHealthyMetrics, true) + } reverseProxyMetrics.upstreamsHealthy.Reset() return &metricsUpstreamsHealthyUpdater{handler} } -func (m *metricsUpstreamsHealthyUpdater) Init() { +func (m *metricsUpstreamsHealthyUpdater) init() { go func() { defer func() { if err := recover(); err != nil { diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index a15dbe424..d799c5a6e 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -382,8 +382,8 @@ func (h *Handler) Provision(ctx caddy.Context) error { } } - upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h) - upstreamHealthyUpdater.Init() + upstreamHealthyUpdater := newMetricsUpstreamsHealthyUpdater(h, ctx) + upstreamHealthyUpdater.init() return nil } From cfc3af67492eba22686fd13a2b2201c66cd737f3 Mon Sep 17 00:00:00 2001 From: Sander Bruens Date: Tue, 28 Jan 2025 16:19:02 -0500 Subject: [PATCH 17/73] fix: update broken link to Ardan Labs (#6800) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abc136f68..3f071e6f0 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ The docs are also open source. You can contribute to them here: https://github.c ## Getting help -- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed. +- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) before help is needed. - A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers! From 9996d6a70ba76a94dfc90548f25fbac0ce9da497 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:25:11 -0700 Subject: [PATCH 18/73] build(deps): bump github.com/golang/glog from 1.2.2 to 1.2.4 (#6814) Bumps [github.com/golang/glog](https://github.com/golang/glog) from 1.2.2 to 1.2.4. - [Release notes](https://github.com/golang/glog/releases) - [Commits](https://github.com/golang/glog/compare/v1.2.2...v1.2.4) --- updated-dependencies: - dependency-name: github.com/golang/glog dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4d9f5ff36..158450951 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.0 require ( github.com/BurntSushi/toml v1.4.0 + github.com/KimMachineGun/automemlimit v0.7.0 github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.14.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b @@ -49,7 +50,6 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect - github.com/KimMachineGun/automemlimit v0.7.0 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -57,7 +57,7 @@ require ( github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-kit/log v0.2.1 // indirect - github.com/golang/glog v1.2.2 // indirect + github.com/golang/glog v1.2.4 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect github.com/google/go-tpm v0.9.0 // indirect github.com/google/go-tspi v0.3.0 // indirect diff --git a/go.sum b/go.sum index 909c7e0b9..8dc6f822d 100644 --- a/go.sum +++ b/go.sum @@ -188,8 +188,8 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= +github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= From 9283770f68f570f47ca20aa9c6f9de8cc50063ba Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Tue, 4 Feb 2025 10:55:30 +0300 Subject: [PATCH 19/73] reverseproxy: ignore duplicate collector registration error (#6820) Signed-off-by: Mohammed Al Sahaf --- modules/caddyhttp/reverseproxy/metrics.go | 36 +++++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/metrics.go b/modules/caddyhttp/reverseproxy/metrics.go index 8a7105c11..248842730 100644 --- a/modules/caddyhttp/reverseproxy/metrics.go +++ b/modules/caddyhttp/reverseproxy/metrics.go @@ -1,11 +1,12 @@ package reverseproxy import ( + "errors" "runtime/debug" + "sync" "time" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -13,6 +14,7 @@ import ( ) var reverseProxyMetrics = struct { + once sync.Once upstreamsHealthy *prometheus.GaugeVec logger *zap.Logger }{} @@ -21,12 +23,25 @@ func initReverseProxyMetrics(handler *Handler, registry *prometheus.Registry) { const ns, sub = "caddy", "reverse_proxy" upstreamsLabels := []string{"upstream"} - reverseProxyMetrics.upstreamsHealthy = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{ - Namespace: ns, - Subsystem: sub, - Name: "upstreams_healthy", - Help: "Health status of reverse proxy upstreams.", - }, upstreamsLabels) + reverseProxyMetrics.once.Do(func() { + reverseProxyMetrics.upstreamsHealthy = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: ns, + Subsystem: sub, + Name: "upstreams_healthy", + Help: "Health status of reverse proxy upstreams.", + }, upstreamsLabels) + }) + + // duplicate registration could happen if multiple sites with reverse proxy are configured; so ignore the error because + // there's no good way to capture having multiple sites with reverse proxy. If this happens, the metrics will be + // registered twice, but the second registration will be ignored. + if err := registry.Register(reverseProxyMetrics.upstreamsHealthy); err != nil && + !errors.Is(err, prometheus.AlreadyRegisteredError{ + ExistingCollector: reverseProxyMetrics.upstreamsHealthy, + NewCollector: reverseProxyMetrics.upstreamsHealthy, + }) { + panic(err) + } reverseProxyMetrics.logger = handler.logger.Named("reverse_proxy.metrics") } @@ -35,13 +50,8 @@ type metricsUpstreamsHealthyUpdater struct { handler *Handler } -const upstreamsHealthyMetrics caddy.CtxKey = "reverse_proxy_upstreams_healthy" - func newMetricsUpstreamsHealthyUpdater(handler *Handler, ctx caddy.Context) *metricsUpstreamsHealthyUpdater { - if set := ctx.Value(upstreamsHealthyMetrics); set == nil { - initReverseProxyMetrics(handler, ctx.GetMetricsRegistry()) - ctx = ctx.WithValue(upstreamsHealthyMetrics, true) - } + initReverseProxyMetrics(handler, ctx.GetMetricsRegistry()) reverseProxyMetrics.upstreamsHealthy.Reset() return &metricsUpstreamsHealthyUpdater{handler} From 96c5c554c1241430ac9ddea6f4b68948adcc961b Mon Sep 17 00:00:00 2001 From: Mahdi Mohammadi <46979964+debug-ing@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:27:32 +0330 Subject: [PATCH 20/73] admin: fix index validation for PUT requests (#6824) --- admin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin.go b/admin.go index 89fce1d28..6df5a23f7 100644 --- a/admin.go +++ b/admin.go @@ -1139,7 +1139,7 @@ traverseLoop: return fmt.Errorf("[%s] invalid array index '%s': %v", path, idxStr, err) } - if idx < 0 || idx >= len(arr) { + if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) { return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr) } } From 932dac157a3c4693b80576477498bb86208b9b30 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 7 Feb 2025 06:18:34 -0700 Subject: [PATCH 21/73] logging: Always set fields func; fix #6829 --- modules/caddyhttp/logging.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 87298ac3c..cef9ea139 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -195,6 +195,7 @@ func (sa *StringArray) UnmarshalJSON(b []byte) error { // have richer information. func errLogValues(err error) (status int, msg string, fields func() []zapcore.Field) { var handlerErr HandlerError + fields = func() []zapcore.Field { return []zapcore.Field{} } // don't be nil (fix #6829) if errors.As(err, &handlerErr) { status = handlerErr.StatusCode if handlerErr.Err == nil { From 9b74a53e510d395e0cbe617a29db6de9828388b8 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 7 Feb 2025 06:23:43 -0700 Subject: [PATCH 22/73] Revert "logging: Always set fields func; fix #6829" This reverts commit 932dac157a3c4693b80576477498bb86208b9b30. Somehow the code I was looking at changed when I committed, without realizing it. This has already been fixed in #6777. --- modules/caddyhttp/logging.go | 1 - modules/caddyhttp/server.go | 1 - 2 files changed, 2 deletions(-) diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index cef9ea139..87298ac3c 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -195,7 +195,6 @@ func (sa *StringArray) UnmarshalJSON(b []byte) error { // have richer information. func errLogValues(err error) (status int, msg string, fields func() []zapcore.Field) { var handlerErr HandlerError - fields = func() []zapcore.Field { return []zapcore.Field{} } // don't be nil (fix #6829) if errors.As(err, &handlerErr) { status = handlerErr.StatusCode if handlerErr.Err == nil { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 12c032dee..a2b29d658 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -408,7 +408,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { if fields == nil { fields = errFields() } - c.Write(fields...) } } From 22563a70eb7b590fcb698680a3ec6d76c0968748 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Mon, 10 Feb 2025 23:39:43 +0800 Subject: [PATCH 23/73] file_server: use the UTC timezone for modified time (#6830) * use UTC timezone for modified time * use http.ParseTime to handle If-Modified-Since * use time.Compare to simplify comparison * take the directory's modtime into consideration when calculating lastModified * update comments about If-Modified-Since's handling --- modules/caddyhttp/fileserver/browse.go | 13 +++++++++---- modules/caddyhttp/fileserver/browsetplcontext.go | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index 0a623c79e..52aa7a9f8 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -130,9 +130,9 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht // speed up browser/client experience and caching by supporting If-Modified-Since if ifModSinceStr := r.Header.Get("If-Modified-Since"); ifModSinceStr != "" { - ifModSince, err := time.ParseInLocation(http.TimeFormat, ifModSinceStr, time.Local) - lastModTrunc := listing.lastModified.Truncate(time.Second) - if err == nil && (lastModTrunc.Equal(ifModSince) || lastModTrunc.Before(ifModSince)) { + // basically a copy of stdlib file server's handling of If-Modified-Since + ifModSince, err := http.ParseTime(ifModSinceStr) + if err == nil && listing.lastModified.Truncate(time.Second).Compare(ifModSince) <= 0 { w.WriteHeader(http.StatusNotModified) return nil } @@ -213,6 +213,11 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht } func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) { + // modTime for the directory itself + stat, err := dir.Stat() + if err != nil { + return nil, err + } dirLimit := defaultDirEntryLimit if fsrv.Browse.FileLimit != 0 { dirLimit = fsrv.Browse.FileLimit @@ -225,7 +230,7 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs // user can presumably browse "up" to parent folder if path is longer than "/" canGoUp := len(urlPath) > 1 - return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil + return fsrv.directoryListing(ctx, fileSystem, stat.ModTime(), files, canGoUp, root, urlPath, repl), nil } // browseApplyQueryParams applies query parameters to the listing. diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index 8e5d138f1..b9489c6a6 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go @@ -35,15 +35,16 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { +func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, parentModTime time.Time, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { filesToHide := fsrv.transformHidePaths(repl) name, _ := url.PathUnescape(urlPath) tplCtx := &browseTemplateContext{ - Name: path.Base(name), - Path: urlPath, - CanGoUp: canGoUp, + Name: path.Base(name), + Path: urlPath, + CanGoUp: canGoUp, + lastModified: parentModTime, } for _, entry := range entries { @@ -131,6 +132,10 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, }) } + // this time is used for the Last-Modified header and comparing If-Modified-Since from client + // both are expected to be in UTC, so we convert to UTC here + // see: https://github.com/caddyserver/caddy/issues/6828 + tplCtx.lastModified = tplCtx.lastModified.UTC() return tplCtx } From 172136a0a0f6aa47be4eab3727fa2482d7af6617 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 11 Feb 2025 22:43:54 -0700 Subject: [PATCH 24/73] caddytls: Support post-quantum key exchange mechanism X25519MLKEM768 Also bump minimum Go version to 1.24. --- go.mod | 4 +--- modules/caddytls/connpolicy.go | 18 ++++++++---------- modules/caddytls/values.go | 26 ++++++++++++-------------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 158450951..e6cfefce0 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/caddyserver/caddy/v2 -go 1.22.3 - -toolchain go1.23.0 +go 1.24 require ( github.com/BurntSushi/toml v1.4.0 diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index d9fc6bcfe..727afaa08 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -884,19 +884,17 @@ func setDefaultTLSParams(cfg *tls.Config) { cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...) if len(cfg.CurvePreferences) == 0 { - // We would want to write - // - // cfg.CurvePreferences = defaultCurves - // - // but that would disable the post-quantum key agreement X25519Kyber768 - // supported in Go 1.23, for which the CurveID is not exported. - // Instead, we'll set CurvePreferences to nil, which will enable PQC. - // See https://github.com/caddyserver/caddy/issues/6540 - cfg.CurvePreferences = nil + cfg.CurvePreferences = defaultCurves } if cfg.MinVersion == 0 { - cfg.MinVersion = tls.VersionTLS12 + // crypto/tls docs: + // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13." + if cfg.EncryptedClientHelloKeys == nil { + cfg.MinVersion = tls.VersionTLS12 + } else { + cfg.MinVersion = tls.VersionTLS13 + } } if cfg.MaxVersion == 0 { cfg.MaxVersion = tls.VersionTLS13 diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 20fe45ff8..2f03d254e 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -81,13 +81,15 @@ func getOptimalDefaultCipherSuites() []uint16 { return defaultCipherSuitesWithoutAESNI } -// SupportedCurves is the unordered map of supported curves. +// SupportedCurves is the unordered map of supported curves +// or key exchange mechanisms ("curves" traditionally). // https://golang.org/pkg/crypto/tls/#CurveID var SupportedCurves = map[string]tls.CurveID{ - "x25519": tls.X25519, - "secp256r1": tls.CurveP256, - "secp384r1": tls.CurveP384, - "secp521r1": tls.CurveP521, + "X25519mlkem768": tls.X25519MLKEM768, + "x25519": tls.X25519, + "secp256r1": tls.CurveP256, + "secp384r1": tls.CurveP384, + "secp521r1": tls.CurveP521, } // supportedCertKeyTypes is all the key types that are supported @@ -100,20 +102,16 @@ var supportedCertKeyTypes = map[string]certmagic.KeyType{ "ed25519": certmagic.ED25519, } -// defaultCurves is the list of only the curves we want to use -// by default, in descending order of preference. +// defaultCurves is the list of only the curves or key exchange +// mechanisms we want to use by default. The order is irrelevant. // -// This list should only include curves which are fast by design -// (e.g. X25519) and those for which an optimized assembly +// This list should only include mechanisms which are fast by +// design (e.g. X25519) and those for which an optimized assembly // implementation exists (e.g. P256). The latter ones can be // found here: // https://github.com/golang/go/tree/master/src/crypto/elliptic -// -// Temporily we ignore these default, to take advantage of X25519Kyber768 -// in Go's defaults (X25519Kyber768, X25519, P-256, P-384, P-521), which -// isn't exported. See https://github.com/caddyserver/caddy/issues/6540 -// nolint:unused var defaultCurves = []tls.CurveID{ + tls.X25519MLKEM768, tls.X25519, tls.CurveP256, } From d7621fdbe65fc084a257e569d1add39dc9a0ce09 Mon Sep 17 00:00:00 2001 From: Gaurav Dhameeja Date: Wed, 12 Feb 2025 15:39:47 +0400 Subject: [PATCH 25/73] tests: tests for error handling & metrics in admin endpoints (#6805) * feat/tests: tests for error handling & metrics in admin endpoints - TestAdminHandlerErrorHandling - Tests the handler.handleError() functionality by directly verifying error response formatting - TestAdminHandlerBuiltinRouteErrors - Tests the error handling pathway by using real admin server routes and verifying both error responses and prometheus metrics increments - provisionAdminRouters: add unit tests for admin handler registration and routing for admin.api - TestAllowedOriginsUnixSocket: checks unix socket with default origins are added - TestReplaceRemoteAdminServer: test for replaceRemoteAdminServer with certificate validation, custom origins and cleanup * test: added test for manage manageIdentity --------- Co-authored-by: Mohammed Al Sahaf --- admin_test.go | 734 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 734 insertions(+) diff --git a/admin_test.go b/admin_test.go index 9137a8881..b00cfaae2 100644 --- a/admin_test.go +++ b/admin_test.go @@ -15,12 +15,19 @@ package caddy import ( + "context" + "crypto/x509" "encoding/json" "fmt" "net/http" + "net/http/httptest" "reflect" "sync" "testing" + + "github.com/caddyserver/certmagic" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" ) var testCfg = []byte(`{ @@ -203,3 +210,730 @@ func BenchmarkLoad(b *testing.B) { Load(testCfg, true) } } + +func TestAdminHandlerErrorHandling(t *testing.T) { + initAdminMetrics() + + handler := adminHandler{ + mux: http.NewServeMux(), + } + + handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := fmt.Errorf("test error") + handler.handleError(w, r, err) + })) + + req := httptest.NewRequest(http.MethodGet, "/error", nil) + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if rr.Code == http.StatusOK { + t.Error("expected error response, got success") + } + + var apiErr APIError + if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil { + t.Fatalf("decoding response: %v", err) + } + if apiErr.Message != "test error" { + t.Errorf("expected error message 'test error', got '%s'", apiErr.Message) + } +} + +func initAdminMetrics() { + if adminMetrics.requestErrors != nil { + prometheus.Unregister(adminMetrics.requestErrors) + } + if adminMetrics.requestCount != nil { + prometheus.Unregister(adminMetrics.requestCount) + } + + adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "caddy", + Subsystem: "admin_http", + Name: "request_errors_total", + Help: "Number of errors that occurred handling admin endpoint requests", + }, []string{"handler", "path", "method"}) + + adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "caddy", + Subsystem: "admin_http", + Name: "requests_total", + Help: "Count of requests to the admin endpoint", + }, []string{"handler", "path", "code", "method"}) // Added code and method labels + + prometheus.MustRegister(adminMetrics.requestErrors) + prometheus.MustRegister(adminMetrics.requestCount) +} + +func TestAdminHandlerBuiltinRouteErrors(t *testing.T) { + initAdminMetrics() + + cfg := &Config{ + Admin: &AdminConfig{ + Listen: "localhost:2019", + }, + } + + err := replaceLocalAdminServer(cfg, Context{}) + if err != nil { + t.Fatalf("setting up admin server: %v", err) + } + defer func() { + stopAdminServer(localAdminServer) + }() + + tests := []struct { + name string + path string + method string + expectedStatus int + }{ + { + name: "stop endpoint wrong method", + path: "/stop", + method: http.MethodGet, + expectedStatus: http.StatusMethodNotAllowed, + }, + { + name: "config endpoint wrong content-type", + path: "/config/", + method: http.MethodPost, + expectedStatus: http.StatusBadRequest, + }, + { + name: "config ID missing ID", + path: "/id/", + method: http.MethodGet, + expectedStatus: http.StatusBadRequest, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil) + rr := httptest.NewRecorder() + + localAdminServer.Handler.ServeHTTP(rr, req) + + if rr.Code != test.expectedStatus { + t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code) + } + + metricValue := testGetMetricValue(map[string]string{ + "path": test.path, + "handler": "admin", + "method": test.method, + }) + if metricValue != 1 { + t.Errorf("expected error metric to be incremented once, got %v", metricValue) + } + }) + } +} + +func testGetMetricValue(labels map[string]string) float64 { + promLabels := prometheus.Labels{} + for k, v := range labels { + promLabels[k] = v + } + + metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels) + if err != nil { + return 0 + } + + pb := &dto.Metric{} + metric.Write(pb) + return pb.GetCounter().GetValue() +} + +type mockRouter struct { + routes []AdminRoute +} + +func (m mockRouter) Routes() []AdminRoute { + return m.routes +} + +type mockModule struct { + mockRouter +} + +func (m *mockModule) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "admin.api.mock", + New: func() Module { + mm := &mockModule{ + mockRouter: mockRouter{ + routes: m.routes, + }, + } + return mm + }, + } +} + +func TestNewAdminHandlerRouterRegistration(t *testing.T) { + originalModules := make(map[string]ModuleInfo) + for k, v := range modules { + originalModules[k] = v + } + defer func() { + modules = originalModules + }() + + mockRoute := AdminRoute{ + Pattern: "/mock", + Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusOK) + return nil + }), + } + + mock := &mockModule{ + mockRouter: mockRouter{ + routes: []AdminRoute{mockRoute}, + }, + } + RegisterModule(mock) + + addr, err := ParseNetworkAddress("localhost:2019") + if err != nil { + t.Fatalf("Failed to parse address: %v", err) + } + + admin := &AdminConfig{ + EnforceOrigin: false, + } + handler := admin.newAdminHandler(addr, false, Context{}) + + req := httptest.NewRequest("GET", "/mock", nil) + req.Host = "localhost:2019" + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code) + t.Logf("Response body: %s", rr.Body.String()) + } + + if len(admin.routers) != 1 { + t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers)) + } +} + +type mockProvisionableRouter struct { + mockRouter + provisionErr error + provisioned bool +} + +func (m *mockProvisionableRouter) Provision(Context) error { + m.provisioned = true + return m.provisionErr +} + +type mockProvisionableModule struct { + *mockProvisionableRouter +} + +func (m *mockProvisionableModule) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "admin.api.mock_provision", + New: func() Module { + mm := &mockProvisionableModule{ + mockProvisionableRouter: &mockProvisionableRouter{ + mockRouter: m.mockRouter, + provisionErr: m.provisionErr, + }, + } + return mm + }, + } +} + +func TestAdminRouterProvisioning(t *testing.T) { + tests := []struct { + name string + provisionErr error + wantErr bool + routersAfter int // expected number of routers after provisioning + }{ + { + name: "successful provisioning", + provisionErr: nil, + wantErr: false, + routersAfter: 0, + }, + { + name: "provisioning error", + provisionErr: fmt.Errorf("provision failed"), + wantErr: true, + routersAfter: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + originalModules := make(map[string]ModuleInfo) + for k, v := range modules { + originalModules[k] = v + } + defer func() { + modules = originalModules + }() + + mockRoute := AdminRoute{ + Pattern: "/mock", + Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error { + return nil + }), + } + + // Create provisionable module + mock := &mockProvisionableModule{ + mockProvisionableRouter: &mockProvisionableRouter{ + mockRouter: mockRouter{ + routes: []AdminRoute{mockRoute}, + }, + provisionErr: test.provisionErr, + }, + } + RegisterModule(mock) + + admin := &AdminConfig{} + addr, err := ParseNetworkAddress("localhost:2019") + if err != nil { + t.Fatalf("Failed to parse address: %v", err) + } + + _ = admin.newAdminHandler(addr, false, Context{}) + err = admin.provisionAdminRouters(Context{}) + + if test.wantErr { + if err == nil { + t.Error("Expected error but got nil") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + + if len(admin.routers) != test.routersAfter { + t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers)) + } + }) + } +} + +func TestAllowedOriginsUnixSocket(t *testing.T) { + tests := []struct { + name string + addr NetworkAddress + origins []string + expectOrigins []string + }{ + { + name: "unix socket with default origins", + addr: NetworkAddress{ + Network: "unix", + Host: "/tmp/caddy.sock", + }, + origins: nil, // default origins + expectOrigins: []string{ + "", // empty host as per RFC 2616 + "127.0.0.1", + "::1", + }, + }, + { + name: "unix socket with custom origins", + addr: NetworkAddress{ + Network: "unix", + Host: "/tmp/caddy.sock", + }, + origins: []string{"example.com"}, + expectOrigins: []string{ + "example.com", + }, + }, + { + name: "tcp socket on localhost gets all loopback addresses", + addr: NetworkAddress{ + Network: "tcp", + Host: "localhost", + StartPort: 2019, + EndPort: 2019, + }, + origins: nil, + expectOrigins: []string{ + "localhost:2019", + "[::1]:2019", + "127.0.0.1:2019", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + admin := AdminConfig{ + Origins: test.origins, + } + + got := admin.allowedOrigins(test.addr) + + var gotOrigins []string + for _, u := range got { + gotOrigins = append(gotOrigins, u.Host) + } + + if len(gotOrigins) != len(test.expectOrigins) { + t.Errorf("Expected %d origins but got %d", len(test.expectOrigins), len(gotOrigins)) + return + } + + expectMap := make(map[string]struct{}) + for _, origin := range test.expectOrigins { + expectMap[origin] = struct{}{} + } + + gotMap := make(map[string]struct{}) + for _, origin := range gotOrigins { + gotMap[origin] = struct{}{} + } + + if !reflect.DeepEqual(expectMap, gotMap) { + t.Errorf("Origins mismatch.\nExpected: %v\nGot: %v", test.expectOrigins, gotOrigins) + } + }) + } +} + +func TestReplaceRemoteAdminServer(t *testing.T) { + const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw +MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX +F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS +VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF +/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU +kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE +g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E +FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG +KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j +oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g +W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q +5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o +BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L +IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ +vq+SH04xKhtFudVBAQ==` + + tests := []struct { + name string + cfg *Config + wantErr bool + }{ + { + name: "nil config", + cfg: nil, + wantErr: false, + }, + { + name: "nil admin config", + cfg: &Config{ + Admin: nil, + }, + wantErr: false, + }, + { + name: "nil remote config", + cfg: &Config{ + Admin: &AdminConfig{}, + }, + wantErr: false, + }, + { + name: "invalid listen address", + cfg: &Config{ + Admin: &AdminConfig{ + Remote: &RemoteAdmin{ + Listen: "invalid:address", + }, + }, + }, + wantErr: true, + }, + { + name: "valid config", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{}, + Remote: &RemoteAdmin{ + Listen: "localhost:2021", + AccessControl: []*AdminAccess{ + { + PublicKeys: []string{testCert}, + Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}}, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "invalid certificate", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{}, + Remote: &RemoteAdmin{ + Listen: "localhost:2021", + AccessControl: []*AdminAccess{ + { + PublicKeys: []string{"invalid-cert-data"}, + Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}}, + }, + }, + }, + }, + }, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := Context{ + Context: context.Background(), + cfg: test.cfg, + } + + if test.cfg != nil { + test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()} + } + + if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil { + identityCertCache = certmagic.NewCache(certmagic.CacheOptions{ + GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) { + return &certmagic.Config{}, nil + }, + }) + } + + err := replaceRemoteAdminServer(ctx, test.cfg) + + if test.wantErr { + if err == nil { + t.Error("Expected error but got nil") + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %v", err) + } + } + + // Clean up + if remoteAdminServer != nil { + _ = stopAdminServer(remoteAdminServer) + } + }) + } +} + +type mockIssuer struct { + configSet *certmagic.Config +} + +func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + return &certmagic.IssuedCertificate{ + Certificate: []byte(csr.Raw), + }, nil +} + +func (m *mockIssuer) SetConfig(cfg *certmagic.Config) { + m.configSet = cfg +} + +func (m *mockIssuer) IssuerKey() string { + return "mock" +} + +type mockIssuerModule struct { + *mockIssuer +} + +func (m *mockIssuerModule) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "tls.issuance.acme", + New: func() Module { + return &mockIssuerModule{mockIssuer: new(mockIssuer)} + }, + } +} + +func TestManageIdentity(t *testing.T) { + originalModules := make(map[string]ModuleInfo) + for k, v := range modules { + originalModules[k] = v + } + defer func() { + modules = originalModules + }() + + RegisterModule(&mockIssuerModule{}) + + certPEM := []byte(`-----BEGIN CERTIFICATE----- +MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE +BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl +cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw +WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN +TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp +bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC +5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9 +8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG +CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t +L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j +b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/ +BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO +ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ +3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE +tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX +7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb +9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +-----END CERTIFICATE-----`) + + keyPEM := []byte(`-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP +... +-----END PRIVATE KEY-----`) + + testStorage := certmagic.FileStorage{Path: t.TempDir()} + err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM) + if err != nil { + t.Fatal(err) + } + err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + cfg *Config + wantErr bool + checkState func(*testing.T, *Config) + }{ + { + name: "nil config", + cfg: nil, + }, + { + name: "nil admin config", + cfg: &Config{ + Admin: nil, + }, + }, + { + name: "nil identity config", + cfg: &Config{ + Admin: &AdminConfig{}, + }, + }, + { + name: "default issuer when none specified", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{ + Identifiers: []string{"localhost"}, + }, + }, + storage: &testStorage, + }, + checkState: func(t *testing.T, cfg *Config) { + if len(cfg.Admin.Identity.issuers) == 0 { + t.Error("Expected at least 1 issuer to be configured") + return + } + if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok { + t.Error("Expected mock issuer to be configured") + } + }, + }, + { + name: "custom issuer", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{ + Identifiers: []string{"localhost"}, + IssuersRaw: []json.RawMessage{ + json.RawMessage(`{"module": "acme"}`), + }, + }, + }, + storage: &certmagic.FileStorage{Path: "testdata"}, + }, + checkState: func(t *testing.T, cfg *Config) { + if len(cfg.Admin.Identity.issuers) != 1 { + t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers)) + } + mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule) + if !ok { + t.Fatal("Expected mock issuer") + } + if mockIss.configSet == nil { + t.Error("Issuer config was not set") + } + }, + }, + { + name: "invalid issuer module", + cfg: &Config{ + Admin: &AdminConfig{ + Identity: &IdentityConfig{ + Identifiers: []string{"localhost"}, + IssuersRaw: []json.RawMessage{ + json.RawMessage(`{"module": "doesnt_exist"}`), + }, + }, + }, + }, + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if identityCertCache != nil { + // Reset the cert cache before each test + identityCertCache.Stop() + identityCertCache = nil + } + + ctx := Context{ + Context: context.Background(), + cfg: test.cfg, + moduleInstances: make(map[string][]Module), + } + + err := manageIdentity(ctx, test.cfg) + + if test.wantErr { + if err == nil { + t.Error("Expected error but got nil") + } + return + } + if err != nil { + t.Fatalf("Expected no error but got: %v", err) + } + + if test.checkState != nil { + test.checkState(t, test.cfg) + } + }) + } +} From 6a8d4f1d60de9419917e771ca027d122000f4c9a Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Mon, 17 Feb 2025 17:58:20 +0300 Subject: [PATCH 26/73] chore: ci: upgrade Go version to 1.24 (#6839) Signed-off-by: Mohammed Al Sahaf --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/cross-build.yml | 10 +++++----- .github/workflows/lint.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c16af8db5..7f8e6d501 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,17 +23,17 @@ jobs: - mac - windows go: - - '1.22' - '1.23' + - '1.24' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.22' - GO_SEMVER: '~1.22.3' - - go: '1.23' - GO_SEMVER: '~1.23.0' + GO_SEMVER: '~1.23.6' + + - go: '1.24' + GO_SEMVER: '~1.24.0' # Set some variables per OS, usable via ${{ matrix.VAR }} # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories) @@ -206,7 +206,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: "~1.23" + go-version: "~1.24" check-latest: true - name: Install xcaddy run: | diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index af0394603..231e4a6e2 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -27,17 +27,17 @@ jobs: - 'darwin' - 'netbsd' go: - - '1.22' - '1.23' + - '1.24' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.22' - GO_SEMVER: '~1.22.3' - - go: '1.23' - GO_SEMVER: '~1.23.0' + GO_SEMVER: '~1.23.6' + + - go: '1.24' + GO_SEMVER: '~1.24.0' runs-on: ubuntu-latest continue-on-error: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 22e13973f..3cfe893df 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '~1.23' + go-version: '~1.24' check-latest: true - name: golangci-lint @@ -63,5 +63,5 @@ jobs: - name: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: '~1.23.0' + go-version-input: '~1.24.0' check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d788ca361..df42679bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,13 @@ jobs: os: - ubuntu-latest go: - - '1.23' + - '1.24' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.23' - GO_SEMVER: '~1.23.0' + - go: '1.24' + GO_SEMVER: '~1.24.0' runs-on: ${{ matrix.os }} # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 From 0d7c63920daecec510202c42816c883fd2dbe047 Mon Sep 17 00:00:00 2001 From: Ns2Kracy <89824014+Ns2Kracy@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:08:39 +0800 Subject: [PATCH 27/73] go.mod: remove glog dependency (#6838) Co-authored-by: Mohammed Al Sahaf Co-authored-by: Matt Holt --- go.mod | 5 ++--- go.sum | 12 ++++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index e6cfefce0..28d7e40a3 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,6 @@ require ( github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-kit/log v0.2.1 // indirect - github.com/golang/glog v1.2.4 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect github.com/google/go-tpm v0.9.0 // indirect github.com/google/go-tspi v0.3.0 // indirect @@ -93,7 +92,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/dgraph-io/badger v1.6.2 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -148,7 +147,7 @@ require ( go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.18.0 // indirect - golang.org/x/sys v0.28.0 + golang.org/x/sys v0.30.0 golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/grpc v1.67.1 // indirect diff --git a/go.sum b/go.sum index 8dc6f822d..c72502433 100644 --- a/go.sum +++ b/go.sum @@ -99,7 +99,6 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -134,8 +133,8 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -188,8 +187,6 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= -github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -667,7 +664,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -679,8 +675,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From fd4de7e0ae4debd54e2a9a4aba10947cd4c3b178 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Thu, 20 Feb 2025 10:45:52 +0100 Subject: [PATCH 28/73] chore: update quic-go to v0.50.0 (#6854) --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 28d7e40a3..deff08696 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 github.com/mholt/acmez/v3 v3.0.1 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.49.0 + github.com/quic-go/quic-go v0.50.0 github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 @@ -125,7 +125,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/client_model v0.5.0 github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rs/xid v1.5.0 // indirect diff --git a/go.sum b/go.sum index c72502433..27b97f31e 100644 --- a/go.sum +++ b/go.sum @@ -393,8 +393,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.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= -github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= +github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= +github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= 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= From 8861eae22350d9e8f94653db951faf85a50a82da Mon Sep 17 00:00:00 2001 From: baruchyahalom Date: Mon, 3 Mar 2025 16:35:54 +0200 Subject: [PATCH 29/73] caddytest: Support configuration defaults override (#6850) --- caddytest/caddytest.go | 63 ++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 05aa1e3f5..623c45e5e 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -31,8 +31,8 @@ import ( _ "github.com/caddyserver/caddy/v2/modules/standard" ) -// Defaults store any configuration required to make the tests run -type Defaults struct { +// Config store any configuration required to make the tests run +type Config struct { // Port we expect caddy to listening on AdminPort int // Certificates we expect to be loaded before attempting to run the tests @@ -44,7 +44,7 @@ type Defaults struct { } // Default testing values -var Default = Defaults{ +var Default = Config{ AdminPort: 2999, // different from what a real server also running on a developer's machine might be Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"}, TestRequestTimeout: 5 * time.Second, @@ -61,6 +61,7 @@ type Tester struct { Client *http.Client configLoaded bool t testing.TB + config Config } // NewTester will create a new testing client with an attached cookie jar @@ -78,9 +79,29 @@ func NewTester(t testing.TB) *Tester { }, configLoaded: false, t: t, + config: Default, } } +// WithDefaultOverrides this will override the default test configuration with the provided values. +func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester { + if overrides.AdminPort != 0 { + tc.config.AdminPort = overrides.AdminPort + } + if len(overrides.Certificates) > 0 { + tc.config.Certificates = overrides.Certificates + } + if overrides.TestRequestTimeout != 0 { + tc.config.TestRequestTimeout = overrides.TestRequestTimeout + tc.Client.Timeout = overrides.TestRequestTimeout + } + if overrides.LoadRequestTimeout != 0 { + tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout + } + + return tc +} + type configLoadError struct { Response string } @@ -113,7 +134,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { return nil } - err := validateTestPrerequisites(tc.t) + err := validateTestPrerequisites(tc) if err != nil { tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err) return nil @@ -121,7 +142,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { tc.t.Cleanup(func() { if tc.t.Failed() && tc.configLoaded { - res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { tc.t.Log("unable to read the current config") return @@ -151,10 +172,10 @@ func (tc *Tester) initServer(rawConfig string, configType string) error { tc.t.Logf("After: %s", rawConfig) } client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + Timeout: tc.config.LoadRequestTimeout, } start := time.Now() - req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig)) + req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.AdminPort), strings.NewReader(rawConfig)) if err != nil { tc.t.Errorf("failed to create request. %s", err) return err @@ -205,11 +226,11 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error } client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + Timeout: tc.config.LoadRequestTimeout, } fetchConfig := func(client *http.Client) any { - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { return nil } @@ -237,30 +258,30 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error } const initConfig = `{ - admin localhost:2999 + admin localhost:%d } ` // validateTestPrerequisites ensures the certificates are available in the // designated path and Caddy sub-process is running. -func validateTestPrerequisites(t testing.TB) error { +func validateTestPrerequisites(tc *Tester) error { // check certificates are found - for _, certName := range Default.Certificates { + for _, certName := range tc.config.Certificates { if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("caddy integration test certificates (%s) not found", certName) } } - if isCaddyAdminRunning() != nil { + if isCaddyAdminRunning(tc) != nil { // setup the init config file, and set the cleanup afterwards f, err := os.CreateTemp("", "") if err != nil { return err } - t.Cleanup(func() { + tc.t.Cleanup(func() { os.Remove(f.Name()) }) - if _, err := f.WriteString(initConfig); err != nil { + if _, err := f.WriteString(fmt.Sprintf(initConfig, tc.config.AdminPort)); err != nil { return err } @@ -271,23 +292,23 @@ func validateTestPrerequisites(t testing.TB) error { }() // wait for caddy to start serving the initial config - for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- { + for retries := 10; retries > 0 && isCaddyAdminRunning(tc) != nil; retries-- { time.Sleep(1 * time.Second) } } // one more time to return the error - return isCaddyAdminRunning() + return isCaddyAdminRunning(tc) } -func isCaddyAdminRunning() error { +func isCaddyAdminRunning(tc *Tester) error { // assert that caddy is running client := &http.Client{ - Timeout: Default.LoadRequestTimeout, + Timeout: tc.config.LoadRequestTimeout, } - resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort)) + resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort)) if err != nil { - return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort) + return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.config.AdminPort) } resp.Body.Close() From ca37c0b05f1817e60331d67d77414cbcf50320c9 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 3 Mar 2025 10:26:39 -0700 Subject: [PATCH 30/73] Fix typo in TLS group x25519mlkem768 --- modules/caddytls/values.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 2f03d254e..3198ffa04 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -85,7 +85,7 @@ func getOptimalDefaultCipherSuites() []uint16 { // or key exchange mechanisms ("curves" traditionally). // https://golang.org/pkg/crypto/tls/#CurveID var SupportedCurves = map[string]tls.CurveID{ - "X25519mlkem768": tls.X25519MLKEM768, + "x25519mlkem768": tls.X25519MLKEM768, "x25519": tls.X25519, "secp256r1": tls.CurveP256, "secp384r1": tls.CurveP384, From 02e348f911d96068c9bb26fd3eefe6cc359c4deb Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Mon, 3 Mar 2025 23:49:17 +0300 Subject: [PATCH 31/73] chore: upgrade cobra (#6868) Signed-off-by: Mohammed Al Sahaf --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index deff08696..27629a5d0 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 - github.com/spf13/cobra v1.8.1 - github.com/spf13/pflag v1.0.5 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.9.0 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 github.com/yuin/goldmark v1.7.8 @@ -89,7 +89,7 @@ require ( github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 github.com/chzyer/readline v1.5.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/dgraph-io/badger v1.6.2 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect diff --git a/go.sum b/go.sum index 27b97f31e..639b9942b 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -469,12 +469,12 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= From eacd7720e99f51b6d2dd340849897c0ff812b8c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:02:16 -0700 Subject: [PATCH 32/73] build(deps): bump github.com/go-jose/go-jose/v3 from 3.0.3 to 3.0.4 (#6871) Bumps [github.com/go-jose/go-jose/v3](https://github.com/go-jose/go-jose) from 3.0.3 to 3.0.4. - [Release notes](https://github.com/go-jose/go-jose/releases) - [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md) - [Commits](https://github.com/go-jose/go-jose/compare/v3.0.3...v3.0.4) --- updated-dependencies: - dependency-name: github.com/go-jose/go-jose/v3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 27629a5d0..729109fdb 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect github.com/google/go-tpm v0.9.0 // indirect diff --git a/go.sum b/go.sum index 639b9942b..f9706143d 100644 --- a/go.sum +++ b/go.sum @@ -160,8 +160,8 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= From d7764dfdbbee04d2f63aa1b05150737dfddc0bcf Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Wed, 5 Mar 2025 17:04:10 -0700 Subject: [PATCH 33/73] caddytls: Encrypted ClientHello (ECH) (#6862) * caddytls: Initial commit of Encrypted ClientHello (ECH) * WIP Caddyfile * Fill out Caddyfile support * Enhance godoc comments * Augment, don't overwrite, HTTPS records * WIP * WIP: publication history * Fix republication logic * Apply global DNS module to ACME challenges This allows DNS challenges to be enabled without locally-configured DNS modules * Ignore false positive from prealloc linter * ci: Use only latest Go version (1.24 currently) We no longer support older Go versions, for security benefits. * Remove old commented code Static ECH keys for now * Implement SendAsRetry --- .github/workflows/ci.yml | 10 +- .github/workflows/cross-build.yml | 10 +- .github/workflows/lint.yml | 2 +- .github/workflows/release.yml | 2 +- caddyconfig/httpcaddyfile/builtins.go | 23 +- caddyconfig/httpcaddyfile/httptype.go | 9 + caddyconfig/httpcaddyfile/options.go | 89 +- caddyconfig/httpcaddyfile/tlsapp.go | 29 +- context.go | 12 +- go.mod | 13 +- go.sum | 26 +- modules/caddyhttp/autohttps.go | 5 + modules/caddytls/acmeissuer.go | 27 +- modules/caddytls/connpolicy.go | 91 ++- modules/caddytls/ech.go | 1074 +++++++++++++++++++++++++ modules/caddytls/ech_test.go | 129 +++ modules/caddytls/tls.go | 106 ++- 17 files changed, 1557 insertions(+), 100 deletions(-) create mode 100644 modules/caddytls/ech.go create mode 100644 modules/caddytls/ech_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f8e6d501..ef7eff943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,22 +18,18 @@ jobs: # Default is true, cancels jobs for other platforms in the matrix if one fails fail-fast: false matrix: - os: + os: - linux - mac - windows - go: - - '1.23' + go: - '1.24' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.23' - GO_SEMVER: '~1.23.6' - - go: '1.24' - GO_SEMVER: '~1.24.0' + GO_SEMVER: '~1.24.1' # Set some variables per OS, usable via ${{ matrix.VAR }} # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories) diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index 231e4a6e2..64ad77bd8 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - goos: + goos: - 'aix' - 'linux' - 'solaris' @@ -26,18 +26,14 @@ jobs: - 'windows' - 'darwin' - 'netbsd' - go: - - '1.23' + go: - '1.24' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.23' - GO_SEMVER: '~1.23.6' - - go: '1.24' - GO_SEMVER: '~1.24.0' + GO_SEMVER: '~1.24.1' runs-on: ubuntu-latest continue-on-error: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3cfe893df..0766f78ac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -63,5 +63,5 @@ jobs: - name: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: '~1.24.0' + go-version-input: '~1.24.1' check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df42679bb..483d1d700 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - go: '1.24' - GO_SEMVER: '~1.24.0' + GO_SEMVER: '~1.24.1' runs-on: ${{ matrix.os }} # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 45570d016..3fc08b2c8 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -99,7 +99,7 @@ func parseBind(h Helper) ([]ConfigValue, error) { // ca // ca_root // key_type [ed25519|p256|p384|rsa2048|rsa4096] -// dns [...] +// dns [ [...]] (required, though, if DNS is not configured as global option) // propagation_delay // propagation_timeout // resolvers @@ -312,10 +312,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) { certManagers = append(certManagers, certManager) case "dns": - if !h.NextArg() { - return nil, h.ArgErr() - } - provName := h.Val() if acmeIssuer == nil { acmeIssuer = new(caddytls.ACMEIssuer) } @@ -325,12 +321,19 @@ func parseTLS(h Helper) ([]ConfigValue, error) { if acmeIssuer.Challenges.DNS == nil { acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) } - modID := "dns.providers." + provName - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) - if err != nil { - return nil, err + // DNS provider configuration optional, since it may be configured globally via the TLS app with global options + if h.NextArg() { + provName := h.Val() + modID := "dns.providers." + provName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) + if err != nil { + return nil, err + } + acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) + } else if h.Option("dns") == nil { + // if DNS is omitted locally, it needs to be configured globally + return nil, h.ArgErr() } - acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) case "resolvers": args := h.RemainingArgs() diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 37a6f6b23..69d88df0c 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -1121,6 +1121,12 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s", cps[i].DefaultSNI, cps[j].DefaultSNI) } + if cps[i].FallbackSNI != "" && + cps[j].FallbackSNI != "" && + cps[i].FallbackSNI != cps[j].FallbackSNI { + return nil, fmt.Errorf("two policies with same match criteria have conflicting fallback SNI: %s vs. %s", + cps[i].FallbackSNI, cps[j].FallbackSNI) + } if cps[i].ProtocolMin != "" && cps[j].ProtocolMin != "" && cps[i].ProtocolMin != cps[j].ProtocolMin { @@ -1161,6 +1167,9 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" { cps[i].DefaultSNI = cps[j].DefaultSNI } + if cps[i].FallbackSNI == "" && cps[j].FallbackSNI != "" { + cps[i].FallbackSNI = cps[j].FallbackSNI + } if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" { cps[i].ProtocolMin = cps[j].ProtocolMin } diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index d4a424624..527676877 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -19,6 +19,7 @@ import ( "strconv" "github.com/caddyserver/certmagic" + "github.com/libdns/libdns" "github.com/mholt/acmez/v3/acme" "github.com/caddyserver/caddy/v2" @@ -45,7 +46,7 @@ func init() { RegisterGlobalOption("ocsp_interval", parseOptDuration) RegisterGlobalOption("acme_ca", parseOptSingleString) RegisterGlobalOption("acme_ca_root", parseOptSingleString) - RegisterGlobalOption("acme_dns", parseOptACMEDNS) + RegisterGlobalOption("acme_dns", parseOptDNS) RegisterGlobalOption("acme_eab", parseOptACMEEAB) RegisterGlobalOption("cert_issuer", parseOptCertIssuer) RegisterGlobalOption("skip_install_trust", parseOptTrue) @@ -62,6 +63,8 @@ func init() { RegisterGlobalOption("log", parseLogOptions) RegisterGlobalOption("preferred_chains", parseOptPreferredChains) RegisterGlobalOption("persist_config", parseOptPersistConfig) + RegisterGlobalOption("dns", parseOptDNS) + RegisterGlobalOption("ech", parseOptECH) } func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } @@ -238,25 +241,6 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) { return caddy.Duration(dur), nil } -func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) { - if !d.Next() { // consume option name - return nil, d.ArgErr() - } - if !d.Next() { // get DNS module name - return nil, d.ArgErr() - } - modID := "dns.providers." + d.Val() - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return nil, err - } - prov, ok := unm.(certmagic.DNSProvider) - if !ok { - return nil, d.Errf("module %s (%T) is not a certmagic.DNSProvider", modID, unm) - } - return prov, nil -} - func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) { eab := new(acme.EAB) d.Next() // consume option name @@ -570,3 +554,68 @@ func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) { d.Next() return caddytls.ParseCaddyfilePreferredChainsOptions(d) } + +func parseOptDNS(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + + if !d.Next() { // get DNS module name + return nil, d.ArgErr() + } + modID := "dns.providers." + d.Val() + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + switch unm.(type) { + case libdns.RecordGetter, + libdns.RecordSetter, + libdns.RecordAppender, + libdns.RecordDeleter: + default: + return nil, d.Errf("module %s (%T) is not a libdns provider", modID, unm) + } + return unm, nil +} + +func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + + ech := new(caddytls.ECH) + + publicNames := d.RemainingArgs() + for _, publicName := range publicNames { + ech.Configs = append(ech.Configs, caddytls.ECHConfiguration{ + OuterSNI: publicName, + }) + } + if len(ech.Configs) == 0 { + return nil, d.ArgErr() + } + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "dns": + if !d.Next() { + return nil, d.ArgErr() + } + providerName := d.Val() + modID := "dns.providers." + providerName + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + ech.Publication = append(ech.Publication, &caddytls.ECHPublication{ + Configs: publicNames, + PublishersRaw: caddy.ModuleMap{ + "dns": caddyconfig.JSON(caddytls.ECHDNSPublisher{ + ProviderRaw: caddyconfig.JSONModuleObject(unm, "name", providerName, nil), + }, nil), + }, + }) + default: + return nil, d.Errf("ech: unrecognized subdirective '%s'", d.Val()) + } + } + + return ech, nil +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 71b524926..adac15065 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -359,6 +359,30 @@ func (st ServerType) buildTLSApp( tlsApp.Automation.OnDemand = onDemand } + // set up "global" (to the TLS app) DNS provider config + if globalDNS, ok := options["dns"]; ok && globalDNS != nil { + tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil) + } + + // set up ECH from Caddyfile options + if ech, ok := options["ech"].(*caddytls.ECH); ok { + tlsApp.EncryptedClientHello = ech + + // outer server names will need certificates, so make sure they're included + // in an automation policy for them that applies any global options + ap, err := newBaseAutomationPolicy(options, warnings, true) + if err != nil { + return nil, warnings, err + } + for _, cfg := range ech.Configs { + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.OuterSNI) + } + if tlsApp.Automation == nil { + tlsApp.Automation = new(caddytls.AutomationConfig) + } + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap) + } + // if the storage clean interval is a boolean, then it's "off" to disable cleaning if sc, ok := options["storage_check"].(string); ok && sc == "off" { tlsApp.DisableStorageCheck = true @@ -553,7 +577,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil { acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference) } - if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) { + // only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others) + if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) { if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } @@ -562,7 +587,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e } acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int) } - if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) { + if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) { if acmeIssuer.Challenges == nil { acmeIssuer.Challenges = new(caddytls.ChallengesConfig) } diff --git a/context.go b/context.go index d4d7afacf..94623df72 100644 --- a/context.go +++ b/context.go @@ -385,6 +385,17 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error return nil, fmt.Errorf("module value cannot be null") } + // if this is an app module, keep a reference to it, + // since submodules may need to reference it during + // provisioning (even though the parent app module + // may not be fully provisioned yet; this is the case + // with the tls app's automation policies, which may + // refer to the tls app to check if a global DNS + // module has been configured for DNS challenges) + if appModule, ok := val.(App); ok { + ctx.cfg.apps[id] = appModule + } + ctx.ancestry = append(ctx.ancestry, val) if prov, ok := val.(Provisioner); ok { @@ -471,7 +482,6 @@ func (ctx Context) App(name string) (any, error) { if appRaw != nil { ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate } - ctx.cfg.apps[name] = modVal.(App) return modVal, nil } diff --git a/go.mod b/go.mod index 729109fdb..f4914bffc 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,9 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma/v2 v2.14.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.21.7 + github.com/caddyserver/certmagic v0.21.8-0.20250220203412-a7894dd6992d github.com/caddyserver/zerossl v0.1.3 + github.com/cloudflare/circl v1.3.3 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 github.com/google/cel-go v0.21.0 @@ -36,11 +37,11 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.33.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 golang.org/x/net v0.33.0 - golang.org/x/sync v0.10.0 - golang.org/x/term v0.27.0 + golang.org/x/sync v0.11.0 + golang.org/x/term v0.29.0 golang.org/x/time v0.7.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -114,7 +115,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.2 + github.com/libdns/libdns v0.2.3 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 @@ -148,7 +149,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.18.0 // indirect golang.org/x/sys v0.30.0 - golang.org/x/text v0.21.0 // indirect + golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect diff --git a/go.sum b/go.sum index f9706143d..ef978d023 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,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.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= -github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= +github.com/caddyserver/certmagic v0.21.8-0.20250220203412-a7894dd6992d h1:9zdfQHH838+rS8pmJ73/RSjpbfHGAyxRX1E79F+1zso= +github.com/caddyserver/certmagic v0.21.8-0.20250220203412-a7894dd6992d/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= 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= @@ -111,6 +111,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -323,8 +325,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.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= -github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +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/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= @@ -596,8 +598,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA= golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -645,8 +647,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -683,8 +685,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -695,8 +697,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 4449e1f4d..dce21a721 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -205,6 +205,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // for all the hostnames we found, filter them so we have // a deduplicated list of names for which to obtain certs // (only if cert management not disabled for this server) + var echDomains []string if srv.AutoHTTPS.DisableCerts { logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) } else { @@ -231,10 +232,14 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } uniqueDomainsForCerts[d] = struct{}{} + echDomains = append(echDomains, d) } } } + // let the TLS server know we have some hostnames that could be protected behind ECH + app.tlsApp.RegisterServerNames(echDomains) + // tell the server to use TLS if it is not already doing so if srv.TLSConnPolicies == nil { srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)} diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 2fe5eec97..c28790fe9 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -146,15 +146,30 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.AccountKey = accountKey } - // DNS providers - if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.ProviderRaw != nil { - val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") - if err != nil { - return fmt.Errorf("loading DNS provider module: %v", err) + // DNS challenge provider + if iss.Challenges != nil && iss.Challenges.DNS != nil { + var prov certmagic.DNSProvider + if iss.Challenges.DNS.ProviderRaw != nil { + // a challenge provider has been locally configured - use it + val, err := ctx.LoadModule(iss.Challenges.DNS, "ProviderRaw") + if err != nil { + return fmt.Errorf("loading DNS provider module: %v", err) + } + prov = val.(certmagic.DNSProvider) + } else if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil { + // no locally configured DNS challenge provider, but if there is + // a global DNS module configured with the TLS app, use that + tlsApp := tlsAppIface.(*TLS) + if tlsApp.dns != nil { + prov = tlsApp.dns.(certmagic.DNSProvider) + } + } + if prov == nil { + return fmt.Errorf("DNS challenge enabled, but no DNS provider configured") } iss.Challenges.DNS.solver = &certmagic.DNS01Solver{ DNSManager: certmagic.DNSManager{ - DNSProvider: val.(certmagic.DNSProvider), + DNSProvider: prov, TTL: time.Duration(iss.Challenges.DNS.TTL), PropagationDelay: time.Duration(iss.Challenges.DNS.PropagationDelay), PropagationTimeout: time.Duration(iss.Challenges.DNS.PropagationTimeout), diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 727afaa08..b686090e6 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -93,7 +93,7 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { // TLSConfig returns a standard-lib-compatible TLS configuration which // selects the first matching policy based on the ClientHello. -func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { +func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // using ServerName to match policies is extremely common, especially in configs // with lots and lots of different policies; we can fast-track those by indexing // them by SNI, so we don't have to iterate potentially thousands of policies @@ -104,6 +104,7 @@ func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { for _, m := range p.matchers { if sni, ok := m.(MatchServerName); ok { for _, sniName := range sni { + // index for fast lookups during handshakes indexedBySNI[sniName] = append(indexedBySNI[sniName], p) } } @@ -111,32 +112,79 @@ func (cp ConnectionPolicies) TLSConfig(_ caddy.Context) *tls.Config { } } - return &tls.Config{ - MinVersion: tls.VersionTLS12, - GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) { - // filter policies by SNI first, if possible, to speed things up - // when there may be lots of policies - possiblePolicies := cp - if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { - possiblePolicies = indexedPolicies - } + getConfigForClient := func(hello *tls.ClientHelloInfo) (*tls.Config, error) { + // filter policies by SNI first, if possible, to speed things up + // when there may be lots of policies + possiblePolicies := cp + if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { + possiblePolicies = indexedPolicies + } - policyLoop: - for _, pol := range possiblePolicies { - for _, matcher := range pol.matchers { - if !matcher.Match(hello) { - continue policyLoop + policyLoop: + for _, pol := range possiblePolicies { + for _, matcher := range pol.matchers { + if !matcher.Match(hello) { + continue policyLoop + } + } + if pol.Drop { + return nil, fmt.Errorf("dropping connection") + } + return pol.TLSConfig, nil + } + + return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) + } + + tlsCfg := &tls.Config{ + MinVersion: tls.VersionTLS12, + GetConfigForClient: getConfigForClient, + } + + // enable ECH, if configured + if tlsAppIface, err := ctx.AppIfConfigured("tls"); err == nil { + tlsApp := tlsAppIface.(*TLS) + + if tlsApp.EncryptedClientHello != nil && len(tlsApp.EncryptedClientHello.configs) > 0 { + // if no publication was configured, we apply ECH to all server names by default, + // but the TLS app needs to know what they are in this case, since they don't appear + // 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 { + for _, m := range p.matchers { + if sni, ok := m.(MatchServerName); ok { + for _, name := range sni { + finalName := strings.ToLower(repl.ReplaceAll(name, "")) + echNames = append(echNames, finalName) + } + } } } - if pol.Drop { - return nil, fmt.Errorf("dropping connection") - } - return pol.TLSConfig, nil + tlsApp.RegisterServerNames(echNames) } - return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) - }, + // TODO: Ideally, ECH keys should be rotated. However, as of Go 1.24, the std lib implementation + // does not support safely modifying the tls.Config's EncryptedClientHelloKeys field. + // So, we implement static ECH keys temporarily. See https://github.com/golang/go/issues/71920. + // Revisit this after Go 1.25 is released and implement key rotation. + var stdECHKeys []tls.EncryptedClientHelloKey + for _, echConfigs := range tlsApp.EncryptedClientHello.configs { + for _, c := range echConfigs { + stdECHKeys = append(stdECHKeys, tls.EncryptedClientHelloKey{ + Config: c.configBin, + PrivateKey: c.privKeyBin, + SendAsRetry: c.sendAsRetry, + }) + } + } + tlsCfg.EncryptedClientHelloKeys = stdECHKeys + } } + + return tlsCfg } // ConnectionPolicy specifies the logic for handling a TLS handshake. @@ -409,6 +457,7 @@ func (p ConnectionPolicy) SettingsEmpty() bool { p.ProtocolMax == "" && p.ClientAuthentication == nil && p.DefaultSNI == "" && + p.FallbackSNI == "" && p.InsecureSecretsLog == "" } diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go new file mode 100644 index 000000000..05a375d9b --- /dev/null +++ b/modules/caddytls/ech.go @@ -0,0 +1,1074 @@ +package caddytls + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/fs" + weakrand "math/rand/v2" + "path" + "strconv" + "strings" + "time" + + "github.com/caddyserver/certmagic" + "github.com/cloudflare/circl/hpke" + "github.com/cloudflare/circl/kem" + "github.com/libdns/libdns" + "go.uber.org/zap" + "golang.org/x/crypto/cryptobyte" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(ECHDNSPublisher{}) +} + +// ECH enables Encrypted ClientHello (ECH) and configures its management. +// +// Note that, as of Caddy 2.10 (~March 2025), ECH keys are not automatically +// rotated due to a limitation in the Go standard library (see +// https://github.com/golang/go/issues/71920). This should be resolved when +// Go 1.25 is released (~Aug. 2025), and Caddy will be updated to automatically +// rotate ECH keys/configs at that point. +// +// EXPERIMENTAL: Subject to change. +type ECH struct { + // The list of ECH configurations for which to automatically generate + // and rotate keys. At least one is required to enable ECH. + Configs []ECHConfiguration `json:"configs,omitempty"` + + // Publication describes ways to publish ECH configs for clients to + // discover and use. Without publication, most clients will not use + // ECH at all, and those that do will suffer degraded performance. + // + // Most major browsers support ECH by way of publication to HTTPS + // DNS RRs. (This also typically requires that they use DoH or DoT.) + Publication []*ECHPublication `json:"publication,omitempty"` + + // map of public_name to list of configs + configs map[string][]echConfig +} + +// Provision loads or creates ECH configs and returns outer names (for certificate +// management), but does not publish any ECH configs. The DNS module is used as +// a default for later publishing if needed. +func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { + logger := ctx.Logger().Named("ech") + + // set up publication modules before we need to obtain a lock in storage, + // since this is strictly internal and doesn't require synchronization + for i, pub := range ech.Publication { + mods, err := ctx.LoadModule(pub, "PublishersRaw") + if err != nil { + return nil, fmt.Errorf("loading ECH publication modules: %v", err) + } + for _, modIface := range mods.(map[string]any) { + ech.Publication[i].publishers = append(ech.Publication[i].publishers, modIface.(ECHPublisher)) + } + } + + // the rest of provisioning needs an exclusive lock so that instances aren't + // stepping on each other when setting up ECH configs + storage := ctx.Storage() + const echLockName = "ech_provision" + if err := storage.Lock(ctx, echLockName); err != nil { + return nil, err + } + defer func() { + if err := storage.Unlock(ctx, echLockName); err != nil { + logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + } + }() + + var outerNames []string //nolint:prealloc // (FALSE POSITIVE - see https://github.com/alexkohler/prealloc/issues/30) + + // start by loading all the existing configs (even the older ones on the way out, + // since some clients may still be using them if they haven't yet picked up on the + // new configs) + cfgKeys, err := storage.List(ctx, echConfigsKey, false) + if err != nil && !errors.Is(err, fs.ErrNotExist) { // OK if dir doesn't exist; it will be created + return nil, err + } + for _, cfgKey := range cfgKeys { + cfg, err := loadECHConfig(ctx, path.Base(cfgKey)) + if err != nil { + return nil, err + } + // if any part of the config's folder was corrupted, the load function will + // clean it up and not return an error, since configs are immutable and + // fairly ephemeral... so just check that we actually got a populated config + if cfg.configBin == nil || cfg.privKeyBin == nil { + continue + } + logger.Debug("loaded ECH config", + zap.String("public_name", cfg.RawPublicName), + zap.Uint8("id", cfg.ConfigID)) + ech.configs[cfg.RawPublicName] = append(ech.configs[cfg.RawPublicName], cfg) + outerNames = append(outerNames, cfg.RawPublicName) + } + + // 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.OuterSNI)) + + if list, ok := ech.configs[publicName]; ok && len(list) > 0 { + // at least one config with this public name was loaded, so find the + // most recent one and mark it as active to be used with retries + var mostRecentDate time.Time + var mostRecentIdx int + for i, c := range list { + if mostRecentDate.IsZero() || c.meta.Created.After(mostRecentDate) { + mostRecentDate = c.meta.Created + mostRecentIdx = i + } + } + list[mostRecentIdx].sendAsRetry = true + } else { + // no config with this public name was loaded, so create one + echCfg, err := generateAndStoreECHConfig(ctx, publicName) + if err != nil { + return nil, err + } + logger.Debug("generated new ECH config", + zap.String("public_name", echCfg.RawPublicName), + zap.Uint8("id", echCfg.ConfigID)) + ech.configs[publicName] = append(ech.configs[publicName], echCfg) + outerNames = append(outerNames, publicName) + } + } + + return outerNames, nil +} + +func (t *TLS) publishECHConfigs() error { + logger := t.logger.Named("ech") + + // make publication exclusive, since we don't need to repeat this unnecessarily + storage := t.ctx.Storage() + const echLockName = "ech_publish" + if err := storage.Lock(t.ctx, echLockName); err != nil { + return err + } + defer func() { + if err := storage.Unlock(t.ctx, echLockName); err != nil { + logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + } + }() + + // get the publication config, or use a default if not specified + // (the default publication config should be to publish all ECH + // configs to the app-global DNS provider; if no DNS provider is + // configured, then this whole function is basically a no-op) + publicationList := t.EncryptedClientHello.Publication + if publicationList == nil { + if dnsProv, ok := t.dns.(ECHDNSProvider); ok { + publicationList = []*ECHPublication{ + { + publishers: []ECHPublisher{ + &ECHDNSPublisher{ + provider: dnsProv, + logger: t.logger, + }, + }, + }, + } + } + } + + // for each publication config, build the list of ECH configs to + // publish with it, and figure out which inner names to publish + // to/for, then publish + for _, publication := range publicationList { + // this publication is either configured for specific ECH configs, + // or we just use an implied default of all ECH configs + var echCfgList echConfigList + var configIDs []uint8 // TODO: use IDs or the outer names? + if publication.Configs == nil { + // by default, publish all configs + for _, configs := range t.EncryptedClientHello.configs { + echCfgList = append(echCfgList, configs...) + for _, c := range configs { + configIDs = append(configIDs, c.ConfigID) + } + } + } else { + for _, cfgOuterName := range publication.Configs { + if cfgList, ok := t.EncryptedClientHello.configs[cfgOuterName]; ok { + echCfgList = append(echCfgList, cfgList...) + for _, c := range cfgList { + configIDs = append(configIDs, c.ConfigID) + } + } + } + } + + // marshal the ECH config list as binary for publication + echCfgListBin, err := echCfgList.MarshalBinary() + if err != nil { + return fmt.Errorf("marshaling ECH config list: %v", err) + } + + // now we have our list of ECH configs to publish and the inner names + // to publish for (i.e. the names being protected); iterate each publisher + // and do the publish for any config+name that needs a publish + for _, publisher := range publication.publishers { + publisherKey := publisher.PublisherKey() + + // by default, publish for all (non-outer) server names, unless + // a specific list of names is configured + var serverNamesSet map[string]struct{} + if publication.Domains == nil { + serverNamesSet = make(map[string]struct{}, len(t.serverNames)) + for name := range t.serverNames { + serverNamesSet[name] = struct{}{} + } + } else { + serverNamesSet = make(map[string]struct{}, len(publication.Domains)) + for _, name := range publication.Domains { + serverNamesSet[name] = struct{}{} + } + } + + // remove any domains from the set which have already had all configs in the + // list published by this publisher, to avoid always re-publishing unnecessarily + for configuredInnerName := range serverNamesSet { + allConfigsPublished := true + for _, cfg := range echCfgList { + // TODO: Potentially utilize the timestamp (map value) for recent-enough publication, instead of just checking for existence + if _, ok := cfg.meta.Publications[publisherKey][configuredInnerName]; !ok { + allConfigsPublished = false + break + } + } + if allConfigsPublished { + delete(serverNamesSet, configuredInnerName) + } + } + + // 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", + zap.Uint8s("config_ids", configIDs), + zap.String("publisher", publisherKey)) + continue + } + + // convert the set of names to a slice + dnsNamesToPublish := make([]string, 0, len(serverNamesSet)) + for name := range serverNamesSet { + dnsNamesToPublish = append(dnsNamesToPublish, name) + } + + logger.Debug("publishing ECH config list", + zap.Strings("domains", dnsNamesToPublish), + zap.Uint8s("config_ids", configIDs)) + + // publish this ECH config list with this publisher + pubTime := time.Now() + err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) + if err != nil { + t.logger.Error("publishing ECH configuration list", + zap.Strings("for_domains", publication.Domains), + zap.Error(err)) + } + + // update publication history, so that we don't unnecessarily republish every time + for _, cfg := range echCfgList { + if cfg.meta.Publications == nil { + cfg.meta.Publications = make(publicationHistory) + } + if _, ok := cfg.meta.Publications[publisherKey]; !ok { + cfg.meta.Publications[publisherKey] = make(map[string]time.Time) + } + for _, name := range dnsNamesToPublish { + cfg.meta.Publications[publisherKey][name] = pubTime + } + metaBytes, err := json.Marshal(cfg.meta) + if err != nil { + return fmt.Errorf("marshaling ECH config metadata: %v", err) + } + metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json") + if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil { + return fmt.Errorf("storing updated ECH config metadata: %v", err) + } + } + } + } + + return nil +} + +// loadECHConfig loads the config from storage with the given configID. +// An error is not actually returned in some cases the config fails to +// load because in some cases it just means the config ID folder has +// been cleaned up in storage, maybe due to an incomplete set of keys +// or corrupted contents; in any case, the only rectification is to +// delete it and make new keys (an error IS returned if deleting the +// corrupted keys fails, for example). Check the returned echConfig for +// non-nil privKeyBin and configBin values before using. +func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) { + storage := ctx.Storage() + logger := ctx.Logger() + + cfgIDKey := path.Join(echConfigsKey, configID) + keyKey := path.Join(cfgIDKey, "key.bin") + configKey := path.Join(cfgIDKey, "config.bin") + metaKey := path.Join(cfgIDKey, "meta.json") + + // if loading anything fails, might as well delete this folder and free up + // the config ID; spec is designed to rotate configs frequently anyway + // (I consider it a more serious error if we can't clean up the folder, + // since leaving stray storage keys is confusing) + privKeyBytes, err := storage.Load(ctx, keyKey) + if err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading private key (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH private key; deleting its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + echConfigBytes, err := storage.Load(ctx, configKey) + if err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH config; deleting its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + var cfg echConfig + if err := cfg.UnmarshalBinary(echConfigBytes); err != nil { + delErr := storage.Delete(ctx, cfgIDKey) + if delErr != nil { + return echConfig{}, fmt.Errorf("error loading ECH config (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) + } + logger.Warn("could not load ECH config; deleted its config folder", + zap.String("config_id", configID), + zap.Error(err)) + return echConfig{}, nil + } + metaBytes, err := storage.Load(ctx, metaKey) + 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) + } + logger.Warn("could not load ECH metadata; deleted its config 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) + } + 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 + cfg.configBin = echConfigBytes + cfg.meta = meta + + return cfg, nil +} + +func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, error) { + // Go currently has very strict requirements for server-side ECH configs, + // to quote the Go 1.24 godoc (with typos of AEAD IDs corrected): + // + // "Config should be a marshalled ECHConfig associated with PrivateKey. This + // must match the config provided to clients byte-for-byte. The config + // should only specify the DHKEM(X25519, HKDF-SHA256) KEM ID (0x0020), the + // HKDF-SHA256 KDF ID (0x0001), and a subset of the following AEAD IDs: + // AES-128-GCM (0x0001), AES-256-GCM (0x0002), ChaCha20Poly1305 (0x0003)." + // + // So we need to be sure we generate a config within these parameters + // so the Go TLS server can use it. + + // generate a key pair + const kemChoice = hpke.KEM_X25519_HKDF_SHA256 + publicKey, privateKey, err := kemChoice.Scheme().GenerateKeyPair() + if err != nil { + return echConfig{}, err + } + + // find an available config ID + configID, err := newECHConfigID(ctx) + if err != nil { + return echConfig{}, fmt.Errorf("generating unique config ID: %v", err) + } + + echCfg := echConfig{ + PublicKey: publicKey, + Version: draftTLSESNI22, + ConfigID: configID, + RawPublicName: publicName, + KEMID: kemChoice, + CipherSuites: []hpkeSymmetricCipherSuite{ + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_AES128GCM, + }, + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_AES256GCM, + }, + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_ChaCha20Poly1305, + }, + }, + sendAsRetry: true, + } + meta := echConfigMeta{ + Created: time.Now(), + } + + privKeyBytes, err := privateKey.MarshalBinary() + if err != nil { + return echConfig{}, fmt.Errorf("marshaling ECH private key: %v", err) + } + echConfigBytes, err := echCfg.MarshalBinary() + if err != nil { + return echConfig{}, fmt.Errorf("marshaling ECH config: %v", err) + } + metaBytes, err := json.Marshal(meta) + if err != nil { + return echConfig{}, fmt.Errorf("marshaling ECH config metadata: %v", err) + } + + parentKey := path.Join(echConfigsKey, strconv.Itoa(int(configID))) + keyKey := path.Join(parentKey, "key.bin") + configKey := path.Join(parentKey, "config.bin") + metaKey := path.Join(parentKey, "meta.json") + + if err := ctx.Storage().Store(ctx, keyKey, privKeyBytes); err != nil { + return echConfig{}, fmt.Errorf("storing ECH private key: %v", err) + } + if err := ctx.Storage().Store(ctx, configKey, echConfigBytes); err != nil { + return echConfig{}, fmt.Errorf("storing ECH config: %v", err) + } + if err := ctx.Storage().Store(ctx, metaKey, metaBytes); err != nil { + return echConfig{}, fmt.Errorf("storing ECH config metadata: %v", err) + } + + echCfg.privKeyBin = privKeyBytes + echCfg.configBin = echConfigBytes // this contains the public key + echCfg.meta = meta + + return echCfg, nil +} + +// ECH represents an Encrypted ClientHello configuration. +// +// EXPERIMENTAL: Subject to change. +type ECHConfiguration struct { + // The public server name that will be used in the outer ClientHello. This + // should be a domain name for which this server is authoritative, because + // Caddy will try to provision a certificate for this name. As an outer + // SNI, it is never used for application data (HTTPS, etc.), but it is + // necessary for securely reconciling inconsistent client state without + // breakage and brittleness. + OuterSNI string `json:"outer_sni,omitempty"` +} + +// ECHPublication configures publication of ECH config(s). +type ECHPublication struct { + // TODO: Should these first two fields be called outer_sni and inner_sni ? + + // The list of ECH configurations to publish, identified by public name. + // If not set, all configs will be included for publication by default. + Configs []string `json:"configs,omitempty"` + + // The list of domain names which are protected with the associated ECH + // configurations ("inner names"). Not all publishers may require this + // information, but some, like the DNS publisher, do. (The DNS publisher, + // for example, needs to know for which domain(s) to create DNS records.) + // + // If not set, all server names registered with the TLS module will be + // added to this list implicitly. (Other Caddy apps that use the TLS + // module automatically register their configured server names for this + // purpose. For example, the HTTP server registers the hostnames for + // which it applies automatic HTTPS.) + // + // Names in this list should not appear in any other publication config + // object with the same publishers, since the publications will likely + // overwrite each other. + // + // NOTE: In order to publish ECH configs for domains configured for + // On-Demand TLS that are not explicitly enumerated elsewhere in the + // config, those domain names will have to be listed here. The only + // time Caddy knows which domains it is serving with On-Demand TLS is + // handshake-time, which is too late for publishing ECH configs; it + // means the first connections would not protect the server names, + // revealing that information to observers, and thus defeating the + // purpose of ECH. Hence the need to list them here so Caddy can + // proactively publish ECH configs before clients connect with those + // server names in plaintext. + Domains []string `json:"domains,omitempty"` + + // How to publish the ECH configurations so clients can know to use them. + // Note that ECH configs are only published when they are newly created, + // so adding or changing publishers after the fact will have no effect + // with existing ECH configs. The next time a config is generated (including + // when a key is rotated), the current publication modules will be utilized. + PublishersRaw caddy.ModuleMap `json:"publishers,omitempty" caddy:"namespace=tls.ech.publishers"` + publishers []ECHPublisher +} + +// ECHDNSProvider can service DNS entries for ECH purposes. +type ECHDNSProvider interface { + libdns.RecordGetter + libdns.RecordSetter +} + +// ECHDNSPublisher configures how to publish an ECH configuration to +// DNS records for the specified domains. +// +// EXPERIMENTAL: Subject to change. +type ECHDNSPublisher struct { + // The DNS provider module which will establish the HTTPS record(s). + ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"` + provider ECHDNSProvider + + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (ECHDNSPublisher) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ech.publishers.dns", + New: func() caddy.Module { return new(ECHDNSPublisher) }, + } +} + +func (dnsPub ECHDNSPublisher) Provision(ctx caddy.Context) error { + dnsProvMod, err := ctx.LoadModule(dnsPub, "ProviderRaw") + if err != nil { + return fmt.Errorf("loading ECH DNS provider module: %v", err) + } + prov, ok := dnsProvMod.(ECHDNSProvider) + if !ok { + return fmt.Errorf("ECH DNS provider module is not an ECH DNS Provider: %v", err) + } + dnsPub.provider = prov + dnsPub.logger = ctx.Logger() + return nil +} + +// PublisherKey returns the name of the DNS provider module. +// We intentionally omit specific provider configuration (or a hash thereof, +// since the config is likely sensitive, potentially containing an API key) +// because it is unlikely that specific configuration, such as an API key, +// is relevant to unique key use as an ECH config publisher. +func (dnsPub ECHDNSPublisher) PublisherKey() string { + return string(dnsPub.provider.(caddy.Module).CaddyModule().ID) +} + +// PublishECHConfigList publishes the given ECH config list to the given DNS names. +func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, configListBin []byte) error { + nameservers := certmagic.RecursiveNameservers(nil) // TODO: we could make resolvers configurable + + for _, domain := range innerNames { + zone, err := certmagic.FindZoneByFQDN(ctx, dnsPub.logger, domain, nameservers) + if err != nil { + dnsPub.logger.Error("could not determine zone for domain", + zap.String("domain", domain), + zap.Error(err)) + continue + } + + // get any existing HTTPS record for this domain, and augment + // our ech SvcParamKey with any other existing SvcParams + recs, err := dnsPub.provider.GetRecords(ctx, zone) + if err != nil { + dnsPub.logger.Error("unable to get existing DNS records to publish ECH data to HTTPS DNS record", + zap.String("domain", domain), + zap.Error(err)) + continue + } + relName := libdns.RelativeName(domain+".", zone) + var httpsRec libdns.Record + for _, rec := range recs { + if rec.Name == relName && rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { + httpsRec = rec + } + } + 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 + } + } + + // overwrite only the ech SvcParamKey + params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)} + + // publish record + _, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{ + { + // HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460) + Type: "HTTPS", + Name: relName, + Priority: 2, // allows a manual override with priority 1 + Target: ".", + Value: params.String(), + TTL: 1 * time.Minute, // TODO: for testing only + }, + }) + if err != nil { + dnsPub.logger.Error("unable to publish ECH data to HTTPS DNS record", + zap.String("domain", domain), + zap.Error(err)) + continue + } + } + + return nil +} + +// echConfig represents an ECHConfig from the specification, +// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html). +type echConfig struct { + // "The version of ECH for which this configuration is used. + // The version is the same as the code point for the + // encrypted_client_hello extension. Clients MUST ignore any + // ECHConfig structure with a version they do not support." + Version uint16 + + // The "length" and "contents" fields defined next in the + // structure are implicitly taken care of by cryptobyte + // when encoding the following fields: + + // HpkeKeyConfig fields: + ConfigID uint8 + KEMID hpke.KEM + PublicKey kem.PublicKey + CipherSuites []hpkeSymmetricCipherSuite + + // ECHConfigContents fields: + MaxNameLength uint8 + RawPublicName string + RawExtensions []byte + + // these fields are not part of the spec, but are here for + // our use when setting up TLS servers or maintenance + configBin []byte + privKeyBin []byte + meta echConfigMeta + sendAsRetry bool +} + +func (echCfg echConfig) MarshalBinary() ([]byte, error) { + var b cryptobyte.Builder + if err := echCfg.marshalBinary(&b); err != nil { + return nil, err + } + return b.Bytes() +} + +// UnmarshalBinary decodes the data back into an ECH config. +// +// Borrowed from github.com/OmarTariq612/goech with modifications. +// Original code: Copyright (c) 2023 Omar Tariq AbdEl-Raziq +func (echCfg *echConfig) UnmarshalBinary(data []byte) error { + var content cryptobyte.String + b := cryptobyte.String(data) + + if !b.ReadUint16(&echCfg.Version) { + return errInvalidLen + } + if echCfg.Version != draftTLSESNI22 { + return fmt.Errorf("supported version must be %d: got %d", draftTLSESNI22, echCfg.Version) + } + + if !b.ReadUint16LengthPrefixed(&content) || !b.Empty() { + return errInvalidLen + } + + var t cryptobyte.String + var pk []byte + + if !content.ReadUint8(&echCfg.ConfigID) || + !content.ReadUint16((*uint16)(&echCfg.KEMID)) || + !content.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&pk, len(t)) || + !content.ReadUint16LengthPrefixed(&t) || + len(t)%4 != 0 /* the length of (KDFs and AEADs) must be divisible by 4 */ { + return errInvalidLen + } + + if !echCfg.KEMID.IsValid() { + return fmt.Errorf("invalid KEM ID: %d", echCfg.KEMID) + } + + var err error + if echCfg.PublicKey, err = echCfg.KEMID.Scheme().UnmarshalBinaryPublicKey(pk); err != nil { + return fmt.Errorf("parsing public_key: %w", err) + } + + echCfg.CipherSuites = echCfg.CipherSuites[:0] + + for !t.Empty() { + var hpkeKDF, hpkeAEAD uint16 + if !t.ReadUint16(&hpkeKDF) || !t.ReadUint16(&hpkeAEAD) { + // we have already checked that the length is divisible by 4 + panic("this must not happen") + } + if !hpke.KDF(hpkeKDF).IsValid() { + return fmt.Errorf("invalid KDF ID: %d", hpkeKDF) + } + if !hpke.AEAD(hpkeAEAD).IsValid() { + return fmt.Errorf("invalid AEAD ID: %d", hpkeAEAD) + } + echCfg.CipherSuites = append(echCfg.CipherSuites, hpkeSymmetricCipherSuite{ + KDFID: hpke.KDF(hpkeKDF), + AEADID: hpke.AEAD(hpkeAEAD), + }) + } + + var rawPublicName []byte + if !content.ReadUint8(&echCfg.MaxNameLength) || + !content.ReadUint8LengthPrefixed(&t) || + !t.ReadBytes(&rawPublicName, len(t)) || + !content.ReadUint16LengthPrefixed(&t) || + !t.ReadBytes(&echCfg.RawExtensions, len(t)) || + !content.Empty() { + return errInvalidLen + } + echCfg.RawPublicName = string(rawPublicName) + + return nil +} + +var errInvalidLen = errors.New("invalid length") + +// marshalBinary writes this config to the cryptobyte builder. If there is an error, +// it will occur before any writes have happened. +func (echCfg echConfig) marshalBinary(b *cryptobyte.Builder) error { + pk, err := echCfg.PublicKey.MarshalBinary() + if err != nil { + return err + } + if l := len(echCfg.RawPublicName); l == 0 || l > 255 { + return fmt.Errorf("public name length (%d) must be in the range 1-255", l) + } + + b.AddUint16(echCfg.Version) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { // "length" field + b.AddUint8(echCfg.ConfigID) + b.AddUint16(uint16(echCfg.KEMID)) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(pk) + }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, cs := range echCfg.CipherSuites { + b.AddUint16(uint16(cs.KDFID)) + b.AddUint16(uint16(cs.AEADID)) + } + }) + b.AddUint8(uint8(min(len(echCfg.RawPublicName)+16, 255))) + b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes([]byte(echCfg.RawPublicName)) + }) + b.AddUint16LengthPrefixed(func(child *cryptobyte.Builder) { + child.AddBytes(echCfg.RawExtensions) + }) + }) + + return nil +} + +type hpkeSymmetricCipherSuite struct { + KDFID hpke.KDF + AEADID hpke.AEAD +} + +type echConfigList []echConfig + +func (cl echConfigList) MarshalBinary() ([]byte, error) { + var b cryptobyte.Builder + var err error + + // the list's length prefixes the list, as with most opaque values + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + for _, cfg := range cl { + if err = cfg.marshalBinary(b); err != nil { + break + } + } + }) + if err != nil { + return nil, err + } + + return b.Bytes() +} + +func newECHConfigID(ctx caddy.Context) (uint8, error) { + // uint8 can be 0-255 inclusive + const uint8Range = 256 + + // avoid repeating storage checks + tried := make([]bool, uint8Range) + + // Try to find an available number with random rejection sampling; + // i.e. choose a random number and see if it's already taken. + // The hard limit on how many times we try to find an available + // number is flexible... in theory, assuming uniform distribution, + // 256 attempts should make each possible value show up exactly + // once, but obviously that won't be the case. We can try more + // times to try to ensure that every number gets a chance, which + // is especially useful if few are available, or we can lower it + // if we assume we should have found an available value by then + // and want to limit runtime; for now I choose the middle ground + // and just try as many times as there are possible values. + for i := 0; i < uint8Range && ctx.Err() == nil; i++ { + num := uint8(weakrand.N(uint8Range)) //nolint:gosec + + // don't try the same number a second time + if tried[num] { + continue + } + tried[num] = true + + // check to see if any of the subkeys use this config ID + numStr := strconv.Itoa(int(num)) + trialPath := path.Join(echConfigsKey, numStr) + if ctx.Storage().Exists(ctx, trialPath) { + continue + } + + return num, nil + } + + if err := ctx.Err(); err != nil { + return 0, err + } + + 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 { + // Returns a key that is unique to this publisher and its configuration. + // A publisher's ID combined with its config is a valid key. + // It is used to prevent duplicating publications. + PublisherKey() string + + // Publishes the ECH config list for the given innerNames. Some publishers + // may not need a list of inner/protected names, and can ignore the argument; + // most, however, will want to use it to know which inner names are to be + // associated with the given ECH config list. + PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error +} + +type echConfigMeta struct { + Created time.Time `json:"created"` + Publications publicationHistory `json:"publications"` +} + +// publicationHistory is a map of publisher key to +// map of inner name to timestamp +type publicationHistory map[string]map[string]time.Time + +// The key prefix when putting ECH configs in storage. After this +// comes the config ID. +const echConfigsKey = "ech/configs" + +// https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html +const draftTLSESNI22 = 0xfe0d + +// Interface guard +var _ ECHPublisher = (*ECHDNSPublisher)(nil) diff --git a/modules/caddytls/ech_test.go b/modules/caddytls/ech_test.go new file mode 100644 index 000000000..b722d2fbf --- /dev/null +++ b/modules/caddytls/ech_test.go @@ -0,0 +1,129 @@ +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 abb519eb7..423a2f9a9 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -20,12 +20,15 @@ import ( "encoding/json" "fmt" "log" + "net" "net/http" "runtime/debug" + "strings" "sync" "time" "github.com/caddyserver/certmagic" + "github.com/libdns/libdns" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -79,6 +82,7 @@ type TLS struct { // Disabling OCSP stapling puts clients at greater risk, reduces their // privacy, and usually lowers client performance. It is NOT recommended // to disable this unless you are able to justify the costs. + // // EXPERIMENTAL. Subject to change. DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"` @@ -89,6 +93,7 @@ type TLS struct { // // Disabling these checks should only be done when the storage // can be trusted to have enough capacity and no other problems. + // // EXPERIMENTAL. Subject to change. DisableStorageCheck bool `json:"disable_storage_check,omitempty"` @@ -100,9 +105,23 @@ type TLS struct { // The instance.uuid file is used to identify the instance of Caddy // in a cluster. The last_clean.json file is used to store the last // time the storage was cleaned. + // // EXPERIMENTAL. Subject to change. DisableStorageClean bool `json:"disable_storage_clean,omitempty"` + // Enable Encrypted ClientHello (ECH). ECH protects the server name + // (SNI) and other sensitive parameters of a normally-plaintext TLS + // ClientHello during a handshake. + // + // EXPERIMENTAL: Subject to change. + EncryptedClientHello *ECH `json:"encrypted_client_hello,omitempty"` + + // The default DNS provider module to use when a DNS module is needed. + // + // EXPERIMENTAL: Subject to change. + DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"` + dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) + certificateLoaders []CertificateLoader automateNames []string ctx caddy.Context @@ -111,6 +130,9 @@ type TLS struct { logger *zap.Logger events *caddyevents.App + serverNames map[string]struct{} + serverNamesMu *sync.Mutex + // set of subjects with managed certificates, // and hashes of manually-loaded certificates // (managing's value is an optional issuer key, for distinction) @@ -136,6 +158,40 @@ 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.serverNamesMu = new(sync.Mutex) + + // set up default DNS module, if any, and make sure it implements all the + // common libdns interfaces, since it could be used for a variety of things + // (do this before provisioning other modules, since they may rely on this) + if len(t.DNSRaw) > 0 { + dnsMod, err := ctx.LoadModule(t, "DNSRaw") + if err != nil { + return fmt.Errorf("loading overall DNS provider module: %v", err) + } + switch dnsMod.(type) { + case interface { + libdns.RecordAppender + libdns.RecordDeleter + libdns.RecordGetter + libdns.RecordSetter + }: + default: + return fmt.Errorf("DNS module does not implement the most common libdns interfaces: %T", dnsMod) + } + t.dns = dnsMod + } + + // ECH (Encrypted ClientHello) initialization + if t.EncryptedClientHello != nil { + t.EncryptedClientHello.configs = make(map[string][]echConfig) + outerNames, err := t.EncryptedClientHello.Provision(ctx) + if err != nil { + return fmt.Errorf("provisioning Encrypted ClientHello components: %v", err) + } + // outer names should have certificates to reduce client brittleness + t.automateNames = append(t.automateNames, outerNames...) + } // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ @@ -178,7 +234,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { for i, sub := range *automateNames { subjects[i] = repl.ReplaceAll(sub, "") } - t.automateNames = subjects + t.automateNames = append(t.automateNames, subjects...) } else { return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) } @@ -339,6 +395,16 @@ func (t *TLS) Start() error { return fmt.Errorf("automate: managing %v: %v", t.automateNames, err) } + // publish ECH configs in the background; does not need to block + // server startup, as it could take a while + if t.EncryptedClientHello != nil { + go func() { + if err := t.publishECHConfigs(); err != nil { + t.logger.Named("ech").Error("publication(s) failed", zap.Error(err)) + } + }() + } + if !t.DisableStorageClean { // start the storage cleaner goroutine and ticker, // which cleans out expired certificates and more @@ -422,11 +488,16 @@ func (t *TLS) Cleanup() error { } } } else { - // no more TLS app running, so delete in-memory cert cache - certCache.Stop() - certCacheMu.Lock() - certCache = nil - certCacheMu.Unlock() + // no more TLS app running, so delete in-memory cert cache, if it was created yet + certCacheMu.RLock() + hasCache := certCache != nil + certCacheMu.RUnlock() + if hasCache { + certCache.Stop() + certCacheMu.Lock() + certCache = nil + certCacheMu.Unlock() + } } return nil @@ -478,6 +549,29 @@ func (t *TLS) Manage(names []string) error { return nil } +// 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. +// +// EXPERIMENTAL: This function and its behavior are subject to change. +func (t *TLS) RegisterServerNames(dnsNames []string) { + t.serverNamesMu.Lock() + for _, name := range dnsNames { + host, _, err := net.SplitHostPort(name) + if err != nil { + host = name + } + if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) { + t.serverNames[strings.ToLower(host)] = struct{}{} + } + } + t.serverNamesMu.Unlock() +} + // 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 From 3644ee31cae8e20493d7ccd0c55b0a9c21f20693 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Mar 2025 17:11:11 -0700 Subject: [PATCH 34/73] build(deps): bump github.com/cloudflare/circl from 1.3.3 to 1.3.7 (#6876) Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.3.3 to 1.3.7. - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.3.3...v1.3.7) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f4914bffc..70f7f1483 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/caddyserver/certmagic v0.21.8-0.20250220203412-a7894dd6992d github.com/caddyserver/zerossl v0.1.3 - github.com/cloudflare/circl v1.3.3 + github.com/cloudflare/circl v1.3.7 github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi/v5 v5.0.12 github.com/google/cel-go v0.21.0 diff --git a/go.sum b/go.sum index ef978d023..15641dc4a 100644 --- a/go.sum +++ b/go.sum @@ -111,8 +111,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= From 481bc80d6e2b58aba045423223a2d996236ec984 Mon Sep 17 00:00:00 2001 From: sashaphmn Date: Thu, 6 Mar 2025 10:21:30 +0200 Subject: [PATCH 35/73] readme: update Twitter name and link (#6874) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3f071e6f0..4b79774b4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@
- @caddyserver on Twitter + @caddyserver on Twitter Caddy Forum
Caddy on Sourcegraph @@ -192,8 +192,8 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B **The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH. -- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_ -- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_ +- _Project on X: [@caddyserver](https://x.com/caddyserver)_ +- _Author on X: [@mholt6](https://x.com/mholt6)_ Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company. From 3207769232679b885354e930f5f92d6e0c5b14d6 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 06:51:18 -0700 Subject: [PATCH 36/73] Update min go version in readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b79774b4..4bebaafdb 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ - Fully-managed local CA for internal names & IPs - Can coordinate with other Caddy instances in a cluster - Multi-issuer fallback + - Encrypted ClientHello (ECH) support - **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues - **Production-ready** after serving trillions of requests and managing millions of TLS certificates - **Scales to hundreds of thousands of sites** as proven in production @@ -87,7 +88,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i Requirements: -- [Go 1.22.3 or newer](https://golang.org/dl/) +- [Go 1.24.0 or newer](https://golang.org/dl/) ### For development From a807fe065959baa8ee2ad95156183c0850c2b584 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 08:52:52 -0700 Subject: [PATCH 37/73] caddytls: Enhance ECH documentation --- modules/caddytls/ech.go | 81 +++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 05a375d9b..fa9d374f6 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -29,7 +29,22 @@ func init() { // ECH enables Encrypted ClientHello (ECH) and configures its management. // -// Note that, as of Caddy 2.10 (~March 2025), ECH keys are not automatically +// ECH helps protect site names (also called "server names" or "domain names" +// or "SNI"), which are normally sent over plaintext when establishing a TLS +// connection. With ECH, the true ClientHello is encrypted and wrapped by an +// "outer" ClientHello that uses a more generic, shared server name that is +// publicly known. +// +// Clients need to know which public name (and other parameters) to use when +// connecting to a site with ECH, and the methods for this vary; however, +// major browsers support reading ECH configurations from DNS records (which +// is typically only secure when DNS-over-HTTPS or DNS-over-TLS is enabled in +// the client). Caddy has the ability to automatically publish ECH configs to +// DNS records if a DNS provider is configured either in the TLS app or with +// each individual publication config object. (Requires a custom build with a +// DNS provider module.) +// +// Note that, as of Caddy 2.10.0 (~March 2025), ECH keys are not automatically // rotated due to a limitation in the Go standard library (see // https://github.com/golang/go/issues/71920). This should be resolved when // Go 1.25 is released (~Aug. 2025), and Caddy will be updated to automatically @@ -39,6 +54,11 @@ func init() { type ECH struct { // The list of ECH configurations for which to automatically generate // and rotate keys. At least one is required to enable ECH. + // + // It is strongly recommended to use as few ECH configs as possible + // to maximize the size of your anonymity set (see the ECH specification + // for a definition). Typically, each server should have only one public + // name, i.e. one config in this list. Configs []ECHConfiguration `json:"configs,omitempty"` // Publication describes ways to publish ECH configs for clients to @@ -482,33 +502,49 @@ func generateAndStoreECHConfig(ctx caddy.Context, publicName string) (echConfig, // // EXPERIMENTAL: Subject to change. type ECHConfiguration struct { - // The public server name that will be used in the outer ClientHello. This - // should be a domain name for which this server is authoritative, because - // Caddy will try to provision a certificate for this name. As an outer - // SNI, it is never used for application data (HTTPS, etc.), but it is - // necessary for securely reconciling inconsistent client state without - // breakage and brittleness. - OuterSNI string `json:"outer_sni,omitempty"` + // The public server name (SNI) that will be used in the outer ClientHello. + // This should be a domain name for which this server is authoritative, + // because Caddy will try to provision a certificate for this name. As an + // outer SNI, it is never used for application data (HTTPS, etc.), but it + // is necessary for enabling clients to connect securely in some cases. + // If this field is empty or missing, or if Caddy cannot get a certificate + // for this domain (e.g. the domain's DNS records do not point to this server), + // client reliability becomes brittle, and you risk coercing clients to expose + // true server names in plaintext, which compromises both the privacy of the + // server and makes clients more vulnerable. + PublicName string `json:"public_name"` } -// ECHPublication configures publication of ECH config(s). +// ECHPublication configures publication of ECH config(s). It pairs a list +// of ECH configs with the list of domains they are assigned to protect, and +// describes how to publish those configs for those domains. +// +// Most servers will have only a single publication config, unless their +// domains are spread across multiple DNS providers or require different +// methods of publication. +// +// EXPERIMENTAL: Subject to change. type ECHPublication struct { - // TODO: Should these first two fields be called outer_sni and inner_sni ? - // The list of ECH configurations to publish, identified by public name. // If not set, all configs will be included for publication by default. + // + // It is generally advised to maximize the size of your anonymity set, + // which implies using as few public names as possible for your sites. + // Usually, only a single public name is used to protect all the sites + // for a server + // + // EXPERIMENTAL: This field may be renamed or have its structure changed. Configs []string `json:"configs,omitempty"` - // The list of domain names which are protected with the associated ECH - // configurations ("inner names"). Not all publishers may require this - // information, but some, like the DNS publisher, do. (The DNS publisher, - // for example, needs to know for which domain(s) to create DNS records.) + // The list of ("inner") domain names which are protected with the associated + // ECH configurations. // // If not set, all server names registered with the TLS module will be - // added to this list implicitly. (Other Caddy apps that use the TLS - // module automatically register their configured server names for this - // purpose. For example, the HTTP server registers the hostnames for - // which it applies automatic HTTPS.) + // added to this list implicitly. (This registration is done automatically + // by other Caddy apps that use the TLS module. They should register their + // configured server names for this purpose. For example, the HTTP server + // registers the hostnames for which it applies automatic HTTPS. This is + // not something you, the user, have to do.) Most servers // // Names in this list should not appear in any other publication config // object with the same publishers, since the publications will likely @@ -526,11 +562,8 @@ type ECHPublication struct { // server names in plaintext. Domains []string `json:"domains,omitempty"` - // How to publish the ECH configurations so clients can know to use them. - // Note that ECH configs are only published when they are newly created, - // so adding or changing publishers after the fact will have no effect - // with existing ECH configs. The next time a config is generated (including - // when a key is rotated), the current publication modules will be utilized. + // How to publish the ECH configurations so clients can know to use + // ECH to connect more securely to the server. PublishersRaw caddy.ModuleMap `json:"publishers,omitempty" caddy:"namespace=tls.ech.publishers"` publishers []ECHPublisher } From bc3d497739444a5ce550696b7b0da36e6e3bc777 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 08:54:40 -0700 Subject: [PATCH 38/73] caddytls: Fix broken refactor Not sure how that happened... --- caddyconfig/httpcaddyfile/options.go | 2 +- caddyconfig/httpcaddyfile/tlsapp.go | 2 +- modules/caddytls/ech.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 527676877..e48a52577 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -585,7 +585,7 @@ func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) { publicNames := d.RemainingArgs() for _, publicName := range publicNames { ech.Configs = append(ech.Configs, caddytls.ECHConfiguration{ - OuterSNI: publicName, + PublicName: publicName, }) } if len(ech.Configs) == 0 { diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index adac15065..8a21ca038 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -375,7 +375,7 @@ func (st ServerType) buildTLSApp( return nil, warnings, err } for _, cfg := range ech.Configs { - ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.OuterSNI) + ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) } if tlsApp.Automation == nil { tlsApp.Automation = new(caddytls.AutomationConfig) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index fa9d374f6..25b7a6923 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -136,7 +136,7 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { // current/active, so they can be used for ECH retries for _, cfg := range ech.Configs { - publicName := strings.ToLower(strings.TrimSpace(cfg.OuterSNI)) + publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) if list, ok := ech.configs[publicName]; ok && len(list) > 0 { // at least one config with this public name was loaded, so find the From 1641e76fd742408c85363e4826451ba9ef22bc99 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 09:52:02 -0700 Subject: [PATCH 39/73] go.mod: Upgrade dependencies --- go.mod | 45 ++++++++++++++-------------- go.sum | 94 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 71 insertions(+), 68 deletions(-) diff --git a/go.mod b/go.mod index 70f7f1483..6bd8743e7 100644 --- a/go.mod +++ b/go.mod @@ -4,20 +4,20 @@ go 1.24 require ( github.com/BurntSushi/toml v1.4.0 - github.com/KimMachineGun/automemlimit v0.7.0 + github.com/KimMachineGun/automemlimit v0.7.1 github.com/Masterminds/sprig/v3 v3.3.0 - github.com/alecthomas/chroma/v2 v2.14.0 + github.com/alecthomas/chroma/v2 v2.15.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.21.8-0.20250220203412-a7894dd6992d + github.com/caddyserver/certmagic v0.22.0 github.com/caddyserver/zerossl v0.1.3 - github.com/cloudflare/circl v1.3.7 + github.com/cloudflare/circl v1.6.0 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.0.12 - github.com/google/cel-go v0.21.0 + github.com/go-chi/chi/v5 v5.2.1 + github.com/google/cel-go v0.24.1 github.com/google/uuid v1.6.0 - github.com/klauspost/compress v1.17.11 - github.com/klauspost/cpuid/v2 v2.2.9 - github.com/mholt/acmez/v3 v3.0.1 + github.com/klauspost/compress v1.18.0 + github.com/klauspost/cpuid/v2 v2.2.10 + github.com/mholt/acmez/v3 v3.1.0 github.com/prometheus/client_golang v1.19.1 github.com/quic-go/quic-go v0.50.0 github.com/smallstep/certificates v0.26.1 @@ -25,7 +25,7 @@ require ( github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc @@ -37,17 +37,18 @@ require ( go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.3.0 - golang.org/x/crypto v0.33.0 - golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 - golang.org/x/net v0.33.0 - golang.org/x/sync v0.11.0 - golang.org/x/term v0.29.0 - golang.org/x/time v0.7.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/sync v0.12.0 + golang.org/x/term v0.30.0 + golang.org/x/time v0.11.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 ) require ( + cel.dev/expr v0.19.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect @@ -95,7 +96,7 @@ require ( github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect @@ -120,7 +121,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/miekg/dns v1.1.62 // indirect + github.com/miekg/dns v1.1.63 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -147,10 +148,10 @@ require ( go.step.sm/crypto v0.45.0 go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/sys v0.30.0 - golang.org/x/text v0.22.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sys v0.31.0 + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.31.0 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 15641dc4a..6cf21753d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -31,8 +33,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/KimMachineGun/automemlimit v0.7.0 h1:7G06p/dMSf7G8E6oq+f2uOPuVncFyIlDI/pBWK49u88= -github.com/KimMachineGun/automemlimit v0.7.0/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= +github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -44,11 +46,11 @@ github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2y github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= +github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= @@ -91,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.21.8-0.20250220203412-a7894dd6992d h1:9zdfQHH838+rS8pmJ73/RSjpbfHGAyxRX1E79F+1zso= -github.com/caddyserver/certmagic v0.21.8-0.20250220203412-a7894dd6992d/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= +github.com/caddyserver/certmagic v0.22.0 h1:hi2skv2jouUw9uQUEyYSTTmqPZPHgf61dOANSIVCLOw= +github.com/caddyserver/certmagic v0.22.0/go.mod h1:Vc0msarAPhOagbDc/SU6M2zbzdwVuZ0lkTh2EqtH4vs= 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= @@ -111,8 +113,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -142,8 +144,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -159,8 +161,8 @@ github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1t github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= @@ -204,8 +206,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= -github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= +github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= +github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= @@ -303,10 +305,10 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -345,11 +347,11 @@ 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.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8= -github.com/mholt/acmez/v3 v3.0.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/acmez/v3 v3.1.0 h1:RlOx2SSZ8dIAM5GfkMe8TdaxjjkiHTGorlMUt8GeMzg= +github.com/mholt/acmez/v3 v3.1.0/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.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= -github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -494,8 +496,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= @@ -598,10 +600,10 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA= -golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= @@ -613,8 +615,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -631,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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +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/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= @@ -647,8 +649,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -677,16 +679,16 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -697,12 +699,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -718,8 +720,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 84364ffcd06e35a93c9bb08ed80617bde72d4f74 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 11:40:03 -0700 Subject: [PATCH 40/73] caddypki: Remove lifetime check at Caddyfile parse (fix #6878) The same check is done at provision time of the ACME server, and that is the correct place to do it. --- modules/caddypki/acmeserver/caddyfile.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/caddypki/acmeserver/caddyfile.go b/modules/caddypki/acmeserver/caddyfile.go index c4d111128..c4d85716f 100644 --- a/modules/caddypki/acmeserver/caddyfile.go +++ b/modules/caddypki/acmeserver/caddyfile.go @@ -15,8 +15,6 @@ package acmeserver import ( - "time" - "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddypki" @@ -74,14 +72,10 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error if !h.NextArg() { return nil, h.ArgErr() } - dur, err := caddy.ParseDuration(h.Val()) if err != nil { return nil, err } - if d := time.Duration(ca.IntermediateLifetime); d > 0 && dur > d { - return nil, h.Errf("certificate lifetime (%s) exceeds intermediate certificate lifetime (%s)", dur, d) - } acmeServer.Lifetime = caddy.Duration(dur) case "resolvers": acmeServer.Resolvers = h.RemainingArgs() From a686f7c346fe011ad153a3bd4ac3e31e6758bcce Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 15:11:38 -0700 Subject: [PATCH 41/73] cmd: Only set memory/CPU limits on run (fix #6879) --- cmd/commandfuncs.go | 19 ++++++++++-------- cmd/main.go | 48 ++++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 2adf95bb3..5c20c22ff 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -171,6 +171,9 @@ func cmdStart(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() + logger := caddy.Log() + setResourceLimits(logger) + configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") resumeFlag := fl.Bool("resume") @@ -196,18 +199,18 @@ func cmdRun(fl Flags) (int, error) { config, err = os.ReadFile(caddy.ConfigAutosavePath) if errors.Is(err, fs.ErrNotExist) { // not a bad error; just can't resume if autosave file doesn't exist - caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) + logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) resumeFlag = false } else if err != nil { return caddy.ExitCodeFailedStartup, err } else { if configFlag == "" { - caddy.Log().Info("resuming from last configuration", + logger.Info("resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } else { // if they also specified a config file, user should be aware that we're not // using it (doing so could lead to data/config loss by overwriting!) - caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration", + logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } } @@ -225,7 +228,7 @@ func cmdRun(fl Flags) (int, error) { if pidfileFlag != "" { err := caddy.PIDFile(pidfileFlag) if err != nil { - caddy.Log().Error("unable to write PID file", + logger.Error("unable to write PID file", zap.String("pidfile", pidfileFlag), zap.Error(err)) } @@ -236,7 +239,7 @@ func cmdRun(fl Flags) (int, error) { if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err) } - caddy.Log().Info("serving initial configuration") + logger.Info("serving initial configuration") // if we are to report to another process the successful start // of the server, do so now by echoing back contents of stdin @@ -272,15 +275,15 @@ func cmdRun(fl Flags) (int, error) { switch runtime.GOOS { case "windows": if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG { - caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy") + logger.Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy") } case "plan9": if os.Getenv("home") == "" && !hasXDG { - caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy") + logger.Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy") } default: if os.Getenv("HOME") == "" && !hasXDG { - caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy") + logger.Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy") } } diff --git a/cmd/main.go b/cmd/main.go index c41738be0..11334cb21 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -69,30 +69,6 @@ func Main() { os.Exit(caddy.ExitCodeFailedStartup) } - logger := caddy.Log() - - // Configure the maximum number of CPUs to use to match the Linux container quota (if any) - // See https://pkg.go.dev/runtime#GOMAXPROCS - undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof)) - defer undo() - if err != nil { - caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) - } - - // Configure the maximum memory to use to match the Linux container quota (if any) or system memory - // See https://pkg.go.dev/runtime/debug#SetMemoryLimit - _, _ = memlimit.SetGoMemLimitWithOpts( - memlimit.WithLogger( - slog.New(zapslog.NewHandler(logger.Core())), - ), - memlimit.WithProvider( - memlimit.ApplyFallback( - memlimit.FromCgroup, - memlimit.FromSystem, - ), - ), - ) - if err := defaultFactory.Build().Execute(); err != nil { var exitError *exitError if errors.As(err, &exitError) { @@ -488,6 +464,30 @@ func printEnvironment() { } } +func setResourceLimits(logger *zap.Logger) { + // Configure the maximum number of CPUs to use to match the Linux container quota (if any) + // See https://pkg.go.dev/runtime#GOMAXPROCS + undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof)) + defer undo() + if err != nil { + caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) + } + + // Configure the maximum memory to use to match the Linux container quota (if any) or system memory + // See https://pkg.go.dev/runtime/debug#SetMemoryLimit + _, _ = memlimit.SetGoMemLimitWithOpts( + memlimit.WithLogger( + slog.New(zapslog.NewHandler(logger.Core())), + ), + memlimit.WithProvider( + memlimit.ApplyFallback( + memlimit.FromCgroup, + memlimit.FromSystem, + ), + ), + ) +} + // StringSlice is a flag.Value that enables repeated use of a string flag. type StringSlice []string From 19876208c79a476a46beec2430e554d4161ab426 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 6 Mar 2025 16:47:02 -0700 Subject: [PATCH 42/73] cmd: Promote undo maxProcs func to caller --- cmd/commandfuncs.go | 3 ++- cmd/main.go | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 5c20c22ff..5127c0f90 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -172,7 +172,8 @@ func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() logger := caddy.Log() - setResourceLimits(logger) + undoMaxProcs := setResourceLimits(logger) + defer undoMaxProcs() configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") diff --git a/cmd/main.go b/cmd/main.go index 11334cb21..87fa9fb95 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -464,13 +464,12 @@ func printEnvironment() { } } -func setResourceLimits(logger *zap.Logger) { +func setResourceLimits(logger *zap.Logger) func() { // Configure the maximum number of CPUs to use to match the Linux container quota (if any) // See https://pkg.go.dev/runtime#GOMAXPROCS undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof)) - defer undo() if err != nil { - caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) + logger.Warn("failed to set GOMAXPROCS", zap.Error(err)) } // Configure the maximum memory to use to match the Linux container quota (if any) or system memory @@ -486,6 +485,8 @@ func setResourceLimits(logger *zap.Logger) { ), ), ) + + return undo } // StringSlice is a flag.Value that enables repeated use of a string flag. From adbe7f87e6bda96a1dddd94ecedefe3219a5304d Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 7 Mar 2025 09:46:43 -0700 Subject: [PATCH 43/73] caddytls: Only make DNS solver if not already set (fix #6880) --- modules/caddytls/acmeissuer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index c28790fe9..f0965855a 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -146,8 +146,8 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { iss.AccountKey = accountKey } - // DNS challenge provider - if iss.Challenges != nil && iss.Challenges.DNS != nil { + // DNS challenge provider, if not already established + if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.solver == nil { var prov certmagic.DNSProvider if iss.Challenges.DNS.ProviderRaw != nil { // a challenge provider has been locally configured - use it From d2a2311bfd513d7e5f6d17858e655ed81fd8c1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 7 Mar 2025 18:40:51 +0100 Subject: [PATCH 44/73] ci: fix Go matrix (#6846) Co-authored-by: Matt Holt --- .github/workflows/ci.yml | 10 +++++++--- .github/workflows/cross-build.yml | 4 ++++ .github/workflows/lint.yml | 4 ++++ .github/workflows/release.yml | 4 ++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef7eff943..5f3e98db6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,10 @@ on: - master - 2.* +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + jobs: test: strategy: @@ -95,7 +99,7 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -tags nobadger -trimpath -ldflags="-w -s" -v + go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v - name: Smoke test Caddy working-directory: ./cmd/caddy @@ -118,7 +122,7 @@ jobs: # continue-on-error: true run: | # (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out - go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./... + go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./... # echo "status=$?" >> $GITHUB_OUTPUT # Relevant step if we reinvestigate publishing test/coverage reports @@ -166,7 +170,7 @@ jobs: retries=3 exit_code=0 while ((retries > 0)); do - CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./... + CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./... exit_code=$? if ((exit_code == 0)); then break diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index 64ad77bd8..372cc7652 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -10,6 +10,10 @@ on: - master - 2.* +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + jobs: build: strategy: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0766f78ac..c5c89b502 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,6 +13,10 @@ on: permissions: contents: read +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + jobs: # From https://github.com/golangci/golangci-lint-action golangci: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 483d1d700..b508ba468 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,10 @@ on: tags: - 'v*.*.*' +env: + # https://github.com/actions/setup-go/issues/491 + GOTOOLCHAIN: local + jobs: release: name: Release From 4ebcfed9c942c59f473f12f8108e1d0fa92e0855 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 7 Mar 2025 11:17:14 -0700 Subject: [PATCH 45/73] caddytls: Reorder provisioning steps (fix #6877) Also add a quick check to allow users to load their own certs for ECH (outer) domains. --- modules/caddytls/tls.go | 118 +++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 423a2f9a9..3b04b8785 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -182,17 +182,6 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.dns = dnsMod } - // ECH (Encrypted ClientHello) initialization - if t.EncryptedClientHello != nil { - t.EncryptedClientHello.configs = make(map[string][]echConfig) - outerNames, err := t.EncryptedClientHello.Provision(ctx) - if err != nil { - return fmt.Errorf("provisioning Encrypted ClientHello components: %v", err) - } - // outer names should have certificates to reduce client brittleness - t.automateNames = append(t.automateNames, outerNames...) - } - // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { @@ -243,31 +232,34 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader)) } - // on-demand permission module - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { - if t.Automation.OnDemand.Ask != "" { - return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") - } - val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") + // using the certificate loaders we just initialized, load + // manual/static (unmanaged) certificates - we do this in + // provision so that other apps (such as http) can know which + // certificates have been manually loaded, and also so that + // commands like validate can be a better test + certCacheMu.RLock() + magic := certmagic.New(certCache, certmagic.Config{ + Storage: ctx.Storage(), + Logger: t.logger, + OnEvent: t.onEvent, + OCSP: certmagic.OCSPConfig{ + DisableStapling: t.DisableOCSPStapling, + }, + DisableStorageCheck: t.DisableStorageCheck, + }) + certCacheMu.RUnlock() + for _, loader := range t.certificateLoaders { + certs, err := loader.LoadCertificates() if err != nil { - return fmt.Errorf("loading on-demand TLS permission module: %v", err) + return fmt.Errorf("loading certificates: %v", err) } - t.Automation.OnDemand.permission = val.(OnDemandPermission) - } - - // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { - t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) - if err != nil { - return fmt.Errorf("preparing 'ask' endpoint: %v", err) + for _, cert := range certs { + hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) + if err != nil { + return fmt.Errorf("caching unmanaged certificate: %v", err) + } + t.loaded[hash] = "" } - perm := PermissionByHTTP{ - Endpoint: t.Automation.OnDemand.Ask, - } - if err := perm.Provision(ctx); err != nil { - return fmt.Errorf("provisioning 'ask' module: %v", err) - } - t.Automation.OnDemand.permission = perm } // automation/management policies @@ -302,6 +294,33 @@ func (t *TLS) Provision(ctx caddy.Context) error { } } + // on-demand permission module + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { + if t.Automation.OnDemand.Ask != "" { + return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") + } + val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") + if err != nil { + return fmt.Errorf("loading on-demand TLS permission module: %v", err) + } + t.Automation.OnDemand.permission = val.(OnDemandPermission) + } + + // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { + t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) + if err != nil { + return fmt.Errorf("preparing 'ask' endpoint: %v", err) + } + perm := PermissionByHTTP{ + Endpoint: t.Automation.OnDemand.Ask, + } + if err := perm.Provision(ctx); err != nil { + return fmt.Errorf("provisioning 'ask' module: %v", err) + } + t.Automation.OnDemand.permission = perm + } + // session ticket ephemeral keys (STEK) service and provider if t.SessionTickets != nil { err := t.SessionTickets.provision(ctx) @@ -310,32 +329,19 @@ func (t *TLS) Provision(ctx caddy.Context) error { } } - // load manual/static (unmanaged) certificates - we do this in - // provision so that other apps (such as http) can know which - // certificates have been manually loaded, and also so that - // commands like validate can be a better test - certCacheMu.RLock() - magic := certmagic.New(certCache, certmagic.Config{ - Storage: ctx.Storage(), - Logger: t.logger, - OnEvent: t.onEvent, - OCSP: certmagic.OCSPConfig{ - DisableStapling: t.DisableOCSPStapling, - }, - DisableStorageCheck: t.DisableStorageCheck, - }) - certCacheMu.RUnlock() - for _, loader := range t.certificateLoaders { - certs, err := loader.LoadCertificates() + // ECH (Encrypted ClientHello) initialization + if t.EncryptedClientHello != nil { + t.EncryptedClientHello.configs = make(map[string][]echConfig) + outerNames, err := t.EncryptedClientHello.Provision(ctx) if err != nil { - return fmt.Errorf("loading certificates: %v", err) + return fmt.Errorf("provisioning Encrypted ClientHello components: %v", err) } - for _, cert := range certs { - hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) - if err != nil { - return fmt.Errorf("caching unmanaged certificate: %v", err) + + // outer names should have certificates to reduce client brittleness + for _, outerName := range outerNames { + if !t.HasCertificateForSubject(outerName) { + t.automateNames = append(t.automateNames, outerNames...) } - t.loaded[hash] = "" } } From 1975408d893226dd5781a0aefdc6181426ee9890 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 7 Mar 2025 11:17:49 -0700 Subject: [PATCH 46/73] chore: Remove unnecessary explicit type parameters --- caddyconfig/httpcaddyfile/httptype.go | 4 ++-- modules/caddyhttp/app.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 69d88df0c..ae6f5ddee 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -191,7 +191,7 @@ func (st ServerType) Setup( metrics, _ := options["metrics"].(*caddyhttp.Metrics) for _, s := range servers { if s.Metrics != nil { - metrics = cmp.Or[*caddyhttp.Metrics](metrics, &caddyhttp.Metrics{}) + metrics = cmp.Or(metrics, &caddyhttp.Metrics{}) metrics = &caddyhttp.Metrics{ PerHost: metrics.PerHost || s.Metrics.PerHost, } @@ -350,7 +350,7 @@ func (st ServerType) Setup( // avoid duplicates by sorting + compacting sort.Strings(defaultLog.Exclude) - defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude) + defaultLog.Exclude = slices.Compact(defaultLog.Exclude) } } // we may have not actually added anything, so remove if empty diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 5477ed8fe..cbd168d31 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -207,7 +207,7 @@ func (app *App) Provision(ctx caddy.Context) error { if srv.Metrics != nil { srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead") - app.Metrics = cmp.Or[*Metrics](app.Metrics, &Metrics{ + app.Metrics = cmp.Or(app.Metrics, &Metrics{ init: sync.Once{}, httpMetrics: &httpMetrics{}, }) From 220cd1c2bcecc07bcf6a0141069538c1b1109907 Mon Sep 17 00:00:00 2001 From: WeidiDeng Date: Sat, 8 Mar 2025 02:22:43 +0800 Subject: [PATCH 47/73] reverseproxy: more comments about buffering and add new tests (#6778) Co-authored-by: Matt Holt --- .../caddyhttp/reverseproxy/buffering_test.go | 84 +++++++++++++++++++ .../caddyhttp/reverseproxy/reverseproxy.go | 4 + 2 files changed, 88 insertions(+) create mode 100644 modules/caddyhttp/reverseproxy/buffering_test.go diff --git a/modules/caddyhttp/reverseproxy/buffering_test.go b/modules/caddyhttp/reverseproxy/buffering_test.go new file mode 100644 index 000000000..68514814c --- /dev/null +++ b/modules/caddyhttp/reverseproxy/buffering_test.go @@ -0,0 +1,84 @@ +package reverseproxy + +import ( + "io" + "testing" +) + +type zeroReader struct{} + +func (zeroReader) Read(p []byte) (int, error) { + for i := range p { + p[i] = 0 + } + return len(p), nil +} + +func TestBuffering(t *testing.T) { + var ( + h Handler + zr zeroReader + ) + type args struct { + body io.ReadCloser + limit int64 + } + tests := []struct { + name string + args args + resultCheck func(io.ReadCloser, int64, args) bool + }{ + { + name: "0 limit, body is returned as is", + args: args{ + body: io.NopCloser(&zr), + limit: 0, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + return res == args.body && read == args.limit && read == 0 + }, + }, + { + name: "negative limit, body is read completely", + args: args{ + body: io.NopCloser(io.LimitReader(&zr, 100)), + limit: -1, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + brc, ok := res.(bodyReadCloser) + return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100 + }, + }, + { + name: "positive limit, body is read partially", + args: args{ + body: io.NopCloser(io.LimitReader(&zr, 100)), + limit: 50, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + brc, ok := res.(bodyReadCloser) + return ok && brc.body != nil && brc.buf.Len() == 50 && read == 50 + }, + }, + { + name: "positive limit, body is read completely", + args: args{ + body: io.NopCloser(io.LimitReader(&zr, 100)), + limit: 101, + }, + resultCheck: func(res io.ReadCloser, read int64, args args) bool { + brc, ok := res.(bodyReadCloser) + return ok && brc.body == nil && brc.buf.Len() == 100 && read == 100 + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, read := h.bufferedBody(tt.args.body, tt.args.limit) + if !tt.resultCheck(res, read, tt.args) { + t.Error("Handler.bufferedBody() test failed") + return + } + }) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index d799c5a6e..5bdafa070 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -1234,6 +1234,10 @@ func (h Handler) provisionUpstream(upstream *Upstream) { // then returns a reader for the buffer along with how many bytes were buffered. Always close // the return value when done with it, just like if it was the original body! If limit is 0 // (which it shouldn't be), this function returns its input; i.e. is a no-op, for safety. +// Otherwise, it returns bodyReadCloser, the original body will be closed and body will be nil +// if it's explicitly configured to buffer all or EOF is reached when reading. +// TODO: the error during reading is discarded if the limit is negative, should the error be propagated +// to upstream/downstream? func (h Handler) bufferedBody(originalBody io.ReadCloser, limit int64) (io.ReadCloser, int64) { if limit == 0 { return originalBody, 0 From f4432a306ac59feee1fc45c8efefad3619e37629 Mon Sep 17 00:00:00 2001 From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com> Date: Sat, 8 Mar 2025 21:45:05 +0100 Subject: [PATCH 48/73] caddyfile: add error handling for unrecognized subdirective/options in various modules (#6884) --- caddyconfig/httpcaddyfile/serveroptions.go | 2 ++ modules/caddyhttp/matchers.go | 2 ++ modules/caddytls/internalissuer.go | 3 +++ modules/logging/filewriter.go | 3 +++ modules/logging/netwriter.go | 3 +++ 5 files changed, 13 insertions(+) diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 40a8af209..d60ce51a9 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -246,6 +246,8 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { switch d.Val() { case "per_host": serverOpts.Metrics.PerHost = true + default: + return nil, d.Errf("unrecognized metrics option '%s'", d.Val()) } } diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index e5ca28b95..01bd7b8b4 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -1342,6 +1342,8 @@ func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { case "early_data": var false bool m.HandshakeComplete = &false + default: + return d.Errf("unrecognized option '%s'", d.Val()) } } if d.NextArg() { diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go index b55757af5..be779757a 100644 --- a/modules/caddytls/internalissuer.go +++ b/modules/caddytls/internalissuer.go @@ -178,6 +178,9 @@ func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } iss.SignWithRoot = true + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index ef3211cbb..0999bbfb2 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -313,6 +313,9 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.Errf("negative roll_keep_for duration: %v", keepFor) } fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil diff --git a/modules/logging/netwriter.go b/modules/logging/netwriter.go index dc2b0922c..7d8481e3c 100644 --- a/modules/logging/netwriter.go +++ b/modules/logging/netwriter.go @@ -145,6 +145,9 @@ func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return d.ArgErr() } nw.SoftStart = true + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil From d57ab215a2f198a465ea33abe4588bb5696e7abd Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Sat, 8 Mar 2025 14:18:54 -0700 Subject: [PATCH 49/73] caddytls: Pointer receiver (fix #6885) --- modules/caddytls/ech.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 25b7a6923..10f325565 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -594,7 +594,7 @@ func (ECHDNSPublisher) CaddyModule() caddy.ModuleInfo { } } -func (dnsPub ECHDNSPublisher) Provision(ctx caddy.Context) error { +func (dnsPub *ECHDNSPublisher) Provision(ctx caddy.Context) error { dnsProvMod, err := ctx.LoadModule(dnsPub, "ProviderRaw") if err != nil { return fmt.Errorf("loading ECH DNS provider module: %v", err) From 49f9af9a4ab2a28fa5c445630017f5284a5afa48 Mon Sep 17 00:00:00 2001 From: jjiang-stripe <55402658+jjiang-stripe@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:50:47 -0700 Subject: [PATCH 50/73] caddytls: Fix TrustedCACerts backwards compatibility (#6889) * add failing test * fix ca pool provisioning * remove unused param --- modules/caddytls/connpolicy.go | 8 +++-- modules/caddytls/connpolicy_test.go | 47 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index b686090e6..cf0ef1e0a 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -798,10 +798,14 @@ func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { // if we have TrustedCACerts explicitly set, create an 'inline' CA and return if len(clientauth.TrustedCACerts) > 0 { - clientauth.ca = InlineCAPool{ + caPool := InlineCAPool{ TrustedCACerts: clientauth.TrustedCACerts, } - return nil + err := caPool.Provision(ctx) + if err != nil { + return nil + } + clientauth.ca = caPool } // if we don't have any CARaw set, there's not much work to do diff --git a/modules/caddytls/connpolicy_test.go b/modules/caddytls/connpolicy_test.go index 0caed2899..82ecbc40d 100644 --- a/modules/caddytls/connpolicy_test.go +++ b/modules/caddytls/connpolicy_test.go @@ -20,6 +20,7 @@ import ( "reflect" "testing" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) @@ -278,3 +279,49 @@ func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) { }) } } + +func TestClientAuthenticationProvision(t *testing.T) { + tests := []struct { + name string + ca ClientAuthentication + wantErr bool + }{ + { + name: "specifying both 'CARaw' and 'TrustedCACerts' produces an error", + ca: ClientAuthentication{ + CARaw: json.RawMessage(`{"provider":"inline","trusted_ca_certs":["foo"]}`), + TrustedCACerts: []string{"foo"}, + }, + wantErr: true, + }, + { + name: "specifying both 'CARaw' and 'TrustedCACertPEMFiles' produces an error", + ca: ClientAuthentication{ + CARaw: json.RawMessage(`{"provider":"inline","trusted_ca_certs":["foo"]}`), + TrustedCACertPEMFiles: []string{"foo"}, + }, + wantErr: true, + }, + { + name: "setting 'TrustedCACerts' provisions the cert pool", + ca: ClientAuthentication{ + TrustedCACerts: []string{test_der_1}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.ca.provision(caddy.Context{}) + if (err != nil) != tt.wantErr { + t.Errorf("ClientAuthentication.provision() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if tt.ca.ca.CertPool() == nil { + t.Error("CertPool is nil, expected non-nil value") + } + } + }) + } +} From 39262f86632401ae4915600b042ef5a28141d3d5 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 11 Mar 2025 08:12:42 -0600 Subject: [PATCH 51/73] caddytls: Minor fixes for ECH --- modules/caddytls/acmeissuer.go | 15 ++++---- modules/caddytls/connpolicy.go | 15 +++----- modules/caddytls/ech.go | 62 +++++++++++++++++++++------------- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index f0965855a..a07a34c10 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -507,21 +507,20 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { iss.TrustedRootsPEMFiles = d.RemainingArgs() case "dns": - if !d.NextArg() { - return d.ArgErr() - } - provName := d.Val() if iss.Challenges == nil { iss.Challenges = new(ChallengesConfig) } if iss.Challenges.DNS == nil { iss.Challenges.DNS = new(DNSChallengeConfig) } - unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) - if err != nil { - return err + if d.NextArg() { + provName := d.Val() + unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) + if err != nil { + return err + } + iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) } - iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) case "propagation_delay": if !d.NextArg() { diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index cf0ef1e0a..fb9588c91 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -940,17 +940,10 @@ func setDefaultTLSParams(cfg *tls.Config) { cfg.CurvePreferences = defaultCurves } - if cfg.MinVersion == 0 { - // crypto/tls docs: - // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13." - if cfg.EncryptedClientHelloKeys == nil { - cfg.MinVersion = tls.VersionTLS12 - } else { - cfg.MinVersion = tls.VersionTLS13 - } - } - if cfg.MaxVersion == 0 { - cfg.MaxVersion = tls.VersionTLS13 + // crypto/tls docs: + // "If EncryptedClientHelloKeys is set, MinVersion, if set, must be VersionTLS13." + if cfg.EncryptedClientHelloKeys != nil && cfg.MinVersion != 0 && cfg.MinVersion < tls.VersionTLS13 { + cfg.MinVersion = tls.VersionTLS13 } } diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 10f325565..63b872260 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -44,6 +44,10 @@ func init() { // each individual publication config object. (Requires a custom build with a // DNS provider module.) // +// ECH requires at least TLS 1.3, so any TLS connection policies with ECH +// applied will automatically upgrade the minimum TLS version to 1.3, even if +// configured to a lower version. +// // Note that, as of Caddy 2.10.0 (~March 2025), ECH keys are not automatically // rotated due to a limitation in the Go standard library (see // https://github.com/golang/go/issues/71920). This should be resolved when @@ -294,31 +298,36 @@ func (t *TLS) publishECHConfigs() error { // publish this ECH config list with this publisher pubTime := time.Now() err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) - if err != nil { - t.logger.Error("publishing ECH configuration list", - zap.Strings("for_domains", publication.Domains), + if err == nil { + t.logger.Info("published ECH configuration list", + zap.Strings("domains", publication.Domains), + zap.Uint8s("config_ids", configIDs), zap.Error(err)) - } - - // update publication history, so that we don't unnecessarily republish every time - for _, cfg := range echCfgList { - if cfg.meta.Publications == nil { - cfg.meta.Publications = make(publicationHistory) - } - if _, ok := cfg.meta.Publications[publisherKey]; !ok { - cfg.meta.Publications[publisherKey] = make(map[string]time.Time) - } - for _, name := range dnsNamesToPublish { - cfg.meta.Publications[publisherKey][name] = pubTime - } - metaBytes, err := json.Marshal(cfg.meta) - if err != nil { - return fmt.Errorf("marshaling ECH config metadata: %v", err) - } - metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json") - if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil { - return fmt.Errorf("storing updated ECH config metadata: %v", err) + // update publication history, so that we don't unnecessarily republish every time + for _, cfg := range echCfgList { + if cfg.meta.Publications == nil { + cfg.meta.Publications = make(publicationHistory) + } + if _, ok := cfg.meta.Publications[publisherKey]; !ok { + cfg.meta.Publications[publisherKey] = make(map[string]time.Time) + } + for _, name := range dnsNamesToPublish { + cfg.meta.Publications[publisherKey][name] = pubTime + } + metaBytes, err := json.Marshal(cfg.meta) + if err != nil { + return fmt.Errorf("marshaling ECH config metadata: %v", err) + } + metaKey := path.Join(echConfigsKey, strconv.Itoa(int(cfg.ConfigID)), "meta.json") + if err := t.ctx.Storage().Store(t.ctx, metaKey, metaBytes); err != nil { + return fmt.Errorf("storing updated ECH config metadata: %v", err) + } } + } else { + t.logger.Error("publishing ECH configuration list", + zap.Strings("domains", publication.Domains), + zap.Uint8s("config_ids", configIDs), + zap.Error(err)) } } } @@ -640,6 +649,10 @@ func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNa continue } relName := libdns.RelativeName(domain+".", zone) + // TODO: libdns.RelativeName should probably return "@" instead of "". + if relName == "" { + relName = "@" + } var httpsRec libdns.Record for _, rec := range recs { if rec.Name == relName && rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { @@ -674,8 +687,11 @@ func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNa }, }) if err != nil { + // TODO: Maybe this should just stop and return the error... dnsPub.logger.Error("unable to publish ECH data to HTTPS DNS record", zap.String("domain", domain), + zap.String("zone", zone), + zap.String("dns_record_name", relName), zap.Error(err)) continue } From af2d33afbb52389cda139a6a0fd8a9d65f558676 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 11 Mar 2025 08:52:15 -0600 Subject: [PATCH 52/73] headers: Allow nil HeaderOps (fix #6893) --- modules/caddyhttp/headers/headers.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index c66bd4144..ef9e35e7d 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -78,7 +78,7 @@ func (h Handler) Validate() error { return err } } - if h.Response != nil { + if h.Response != nil && h.Response.HeaderOps != nil { err := h.Response.validate() if err != nil { return err @@ -133,6 +133,9 @@ type HeaderOps struct { // Provision sets up the header operations. func (ops *HeaderOps) Provision(_ caddy.Context) error { + if ops == nil { + return nil // it's possible no ops are configured; fix #6893 + } for fieldName, replacements := range ops.Replace { for i, r := range replacements { if r.SearchRegexp == "" { From dccf3d8982d1b428e840d43f71fa5c3becf6ea8f Mon Sep 17 00:00:00 2001 From: Adrien Pensart Date: Wed, 12 Mar 2025 15:38:51 -0400 Subject: [PATCH 53/73] requestbody: Add set option to replace request body (#5795) Co-authored-by: Matt Holt --- modules/caddyhttp/requestbody/caddyfile.go | 6 ++++++ modules/caddyhttp/requestbody/requestbody.go | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/modules/caddyhttp/requestbody/caddyfile.go b/modules/caddyhttp/requestbody/caddyfile.go index 8378ad7f4..e2382d546 100644 --- a/modules/caddyhttp/requestbody/caddyfile.go +++ b/modules/caddyhttp/requestbody/caddyfile.go @@ -68,6 +68,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) } rb.WriteTimeout = timeout + case "set": + var setStr string + if !h.AllArgs(&setStr) { + return nil, h.ArgErr() + } + rb.Set = setStr default: return nil, h.Errf("unrecognized request_body subdirective '%s'", h.Val()) } diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 830050416..a4518a5b4 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -18,6 +18,7 @@ import ( "errors" "io" "net/http" + "strings" "time" "go.uber.org/zap" @@ -43,6 +44,10 @@ type RequestBody struct { // EXPERIMENTAL. Subject to change/removal. WriteTimeout time.Duration `json:"write_timeout,omitempty"` + // This field permit to replace body on the fly + // EXPERIMENTAL. Subject to change/removal. + Set string `json:"set,omitempty"` + logger *zap.Logger } @@ -60,6 +65,18 @@ func (rb *RequestBody) Provision(ctx caddy.Context) error { } func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + if rb.Set != "" { + if r.Body != nil { + err := r.Body.Close() + if err != nil { + return err + } + } + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + replacedBody := repl.ReplaceAll(rb.Set, "") + r.Body = io.NopCloser(strings.NewReader(replacedBody)) + r.ContentLength = int64(len(rb.Set)) + } if r.Body == nil { return next.ServeHTTP(w, r) } From 2ac09fdb2046957597e17096adf6335a6d589a2f Mon Sep 17 00:00:00 2001 From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:18:02 +0100 Subject: [PATCH 54/73] requestbody: Fix ContentLength calculation after body replacement (#6896) --- modules/caddyhttp/requestbody/requestbody.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index a4518a5b4..1fa654811 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -75,7 +75,7 @@ func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next cad repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) replacedBody := repl.ReplaceAll(rb.Set, "") r.Body = io.NopCloser(strings.NewReader(replacedBody)) - r.ContentLength = int64(len(rb.Set)) + r.ContentLength = int64(len(replacedBody)) } if r.Body == nil { return next.ServeHTTP(w, r) From 1f8dab572ca9681464fdadc65bfb5f250fc496c3 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 12 Mar 2025 16:33:03 -0600 Subject: [PATCH 55/73] caddytls: Don't publish ECH configs if other records don't exist Publishing a DNS record for a name that doesn't have any could make wildcards ineffective, which would be surprising for site owners and could lead to downtime. --- modules/caddyhttp/autohttps.go | 17 ++++++++++----- modules/caddytls/ech.go | 38 ++++++++++++++++++++++++++-------- modules/caddytls/tls.go | 2 +- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index dce21a721..769cfd4ef 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -163,6 +163,7 @@ 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 { @@ -184,6 +185,17 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } + // 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 + echDomains := make([]string, 0, len(serverDomainSet)) + for d := range serverDomainSet { + echDomains = append(echDomains, d) + } + app.tlsApp.RegisterServerNames(echDomains) + // nothing more to do here if there are no domains that qualify for // automatic HTTPS and there are no explicit TLS connection policies: // if there is at least one domain but no TLS conn policy (F&&T), we'll @@ -205,7 +217,6 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // for all the hostnames we found, filter them so we have // a deduplicated list of names for which to obtain certs // (only if cert management not disabled for this server) - var echDomains []string if srv.AutoHTTPS.DisableCerts { logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) } else { @@ -232,14 +243,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } uniqueDomainsForCerts[d] = struct{}{} - echDomains = append(echDomains, d) } } } - // let the TLS server know we have some hostnames that could be protected behind ECH - app.tlsApp.RegisterServerNames(echDomains) - // tell the server to use TLS if it is not already doing so if srv.TLSConnPolicies == nil { srv.TLSConnPolicies = caddytls.ConnectionPolicies{new(caddytls.ConnectionPolicy)} diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 63b872260..a192c3390 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -639,8 +639,16 @@ func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNa continue } - // get any existing HTTPS record for this domain, and augment - // our ech SvcParamKey with any other existing SvcParams + 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 + // also want to get any HTTPS record that may already exist for it so + // we can augment the ech SvcParamKey with any other existing SvcParams recs, err := dnsPub.provider.GetRecords(ctx, zone) if err != nil { dnsPub.logger.Error("unable to get existing DNS records to publish ECH data to HTTPS DNS record", @@ -648,17 +656,29 @@ func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNa zap.Error(err)) continue } - relName := libdns.RelativeName(domain+".", zone) - // TODO: libdns.RelativeName should probably return "@" instead of "". - if relName == "" { - relName = "@" - } var httpsRec libdns.Record + var nameHasExistingRecord bool for _, rec := range recs { - if rec.Name == relName && rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { - httpsRec = rec + if rec.Name == relName { + nameHasExistingRecord = true + if rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { + httpsRec = rec + break + } } } + if !nameHasExistingRecord { + // Turns out if you publish a DNS record for a name that doesn't have any DNS record yet, + // any wildcard records won't apply for the name anymore, meaning if a wildcard A/AAAA record + // is used to resolve the domain to a server, publishing an HTTPS record could break resolution! + // In theory, this should be a non-issue, at least for A/AAAA records, if the HTTPS record + // includes ipv[4|6]hint SvcParamKeys, + dnsPub.logger.Warn("domain does not have any existing records, so skipping publication of HTTPS record", + zap.String("domain", domain), + zap.String("relative_name", relName), + zap.String("zone", zone)) + continue + } params := make(svcParams) if httpsRec.Value != "" { params, err = parseSvcParams(httpsRec.Value) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 3b04b8785..089794efb 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -563,7 +563,7 @@ func (t *TLS) Manage(names []string) error { // (keeping only the hotsname) and filters IP addresses, which can't be // used with ECH. // -// EXPERIMENTAL: This function and its behavior are subject to change. +// EXPERIMENTAL: This function and its semantics/behavior are subject to change. func (t *TLS) RegisterServerNames(dnsNames []string) { t.serverNamesMu.Lock() for _, name := range dnsNames { From 55c89ccf2a39dcfd7286fcaed54787821ff9a1aa Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 14 Mar 2025 15:44:20 -0600 Subject: [PATCH 56/73] caddytls: Convert AP subjects to punycode Fixes bugs related to TLS automation --- modules/caddytls/automation.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 1bc86020d..96fc2ac61 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -28,6 +28,7 @@ import ( "github.com/mholt/acmez/v3" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/net/idna" "github.com/caddyserver/caddy/v2" ) @@ -183,7 +184,12 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { repl := caddy.NewReplacer() subjects := make([]string, len(ap.SubjectsRaw)) for i, sub := range ap.SubjectsRaw { - subjects[i] = repl.ReplaceAll(sub, "") + sub = repl.ReplaceAll(sub, "") + subASCII, err := idna.ToASCII(sub) + if err != nil { + return fmt.Errorf("could not convert automation policy subject '%s' to punycode: %v", sub, err) + } + subjects[i] = subASCII } ap.subjects = subjects From b3e692ed09f8ba15b741621c4b16d8bfee38f8a1 Mon Sep 17 00:00:00 2001 From: Ted Date: Mon, 17 Mar 2025 17:58:46 +0300 Subject: [PATCH 57/73] caddyfile: Fix formatting for backquote wrapped braces (#6903) --- caddyconfig/caddyfile/formatter.go | 17 +++++++++++++++-- caddyconfig/caddyfile/formatter_test.go | 10 ++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index d35f0ac6b..f1d12fa51 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -61,7 +61,8 @@ func Format(input []byte) []byte { heredocMarker []rune heredocClosingMarker []rune - nesting int // indentation level + nesting int // indentation level + withinBackquote bool ) write := func(ch rune) { @@ -88,6 +89,9 @@ func Format(input []byte) []byte { } panic(err) } + if ch == '`' { + withinBackquote = !withinBackquote + } // detect whether we have the start of a heredoc if !quoted && !(heredoc != heredocClosed || heredocEscaped) && @@ -236,14 +240,23 @@ func Format(input []byte) []byte { switch { case ch == '{': openBrace = true - openBraceWritten = false openBraceSpace = spacePrior && !beginningOfLine if openBraceSpace { write(' ') } + openBraceWritten = false + if withinBackquote { + write('{') + openBraceWritten = true + continue + } continue case ch == '}' && (spacePrior || !openBrace): + if withinBackquote { + write('}') + continue + } if last != '\n' { nextLine() } diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index 6eec822fe..a64383c3c 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -434,6 +434,16 @@ block2 { } `, }, + { + description: "Preserve braces wrapped by backquotes", + input: "block {respond `All braces should remain: {{now | date \"2006\"}}`}", + expect: "block {respond `All braces should remain: {{now | date \"2006\"}}`}", + }, + { + description: "Preserve braces wrapped by quotes", + input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", + expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}", + }, } { // the formatter should output a trailing newline, // even if the tests aren't written to expect that From e276994174983dbb190d4bb9acaab157ef14373b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 17 Mar 2025 12:02:23 -0600 Subject: [PATCH 58/73] caddytls: Initialize permission module earlier (fix #6901) Bug introduced in 4ebcfed9c942c59f473f12f8108e1d0fa92e0855 --- modules/caddytls/automation.go | 10 ++++------ modules/caddytls/tls.go | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 96fc2ac61..6f3b98a3e 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -173,9 +173,6 @@ type AutomationPolicy struct { subjects []string magic *certmagic.Config storage certmagic.Storage - - // Whether this policy had explicit managers configured directly on it. - hadExplicitManagers bool } // Provision sets up ap and builds its underlying CertMagic config. @@ -212,8 +209,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // store them on the policy before putting it on the config // load and provision any cert manager modules + var hadExplicitManagers bool if ap.ManagersRaw != nil { - ap.hadExplicitManagers = true + hadExplicitManagers = true vals, err := tlsApp.ctx.LoadModule(ap, "ManagersRaw") if err != nil { return fmt.Errorf("loading external certificate manager modules: %v", err) @@ -273,9 +271,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // prevent issuance from Issuers (when Managers don't provide a certificate) if there's no // permission module configured noProtections := ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) - failClosed := noProtections && !ap.hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured + failClosed := noProtections && !hadExplicitManagers // don't allow on-demand issuance (other than implicit managers) if no managers have been explicitly configured if noProtections { - if !ap.hadExplicitManagers { + if !hadExplicitManagers { // no managers, no explicitly-configured permission module, this is a config error return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 089794efb..0f8433960 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -262,6 +262,18 @@ func (t *TLS) Provision(ctx caddy.Context) error { } } + // on-demand permission module + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { + if t.Automation.OnDemand.Ask != "" { + return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") + } + val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") + if err != nil { + return fmt.Errorf("loading on-demand TLS permission module: %v", err) + } + t.Automation.OnDemand.permission = val.(OnDemandPermission) + } + // automation/management policies if t.Automation == nil { t.Automation = new(AutomationConfig) @@ -294,18 +306,6 @@ func (t *TLS) Provision(ctx caddy.Context) error { } } - // on-demand permission module - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { - if t.Automation.OnDemand.Ask != "" { - return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") - } - val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") - if err != nil { - return fmt.Errorf("loading on-demand TLS permission module: %v", err) - } - t.Automation.OnDemand.permission = val.(OnDemandPermission) - } - // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) From 8dc76676fb5156f971db49c50a5c509d1c93613b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 19 Mar 2025 09:53:42 -0600 Subject: [PATCH 59/73] chore: Modernize a couple for loops --- caddyconfig/httpcaddyfile/tlsapp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 8a21ca038..2eedca453 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -340,7 +340,7 @@ func (st ServerType) buildTLSApp( combined = reflect.New(reflect.TypeOf(cl)).Elem() } clVal := reflect.ValueOf(cl) - for i := 0; i < clVal.Len(); i++ { + for i := range clVal.Len() { combined = reflect.Append(combined, clVal.Index(i)) } loadersByName[name] = combined.Interface().(caddytls.CertificateLoader) @@ -469,7 +469,7 @@ func (st ServerType) buildTLSApp( globalPreferredChains := options["preferred_chains"] hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil if hasGlobalACMEDefaults { - for i := 0; i < len(tlsApp.Automation.Policies); i++ { + for i := range tlsApp.Automation.Policies { ap := tlsApp.Automation.Policies[i] if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) { // for public names, create default issuers which will later be filled in with configured global defaults From 7b1f00c330900b6eb7cfa7464033a008b92c9675 Mon Sep 17 00:00:00 2001 From: Marten Seemann Date: Fri, 21 Mar 2025 20:33:49 +0700 Subject: [PATCH 60/73] update quic-go to v0.50.1 (#6918) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6bd8743e7..4f701c778 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/klauspost/cpuid/v2 v2.2.10 github.com/mholt/acmez/v3 v3.1.0 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.50.0 + github.com/quic-go/quic-go v0.50.1 github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 diff --git a/go.sum b/go.sum index 6cf21753d..990096efd 100644 --- a/go.sum +++ b/go.sum @@ -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.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= -github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +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/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= From 173573035c7484bb4aad4498a90bf5a1cf1bb5be Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Fri, 21 Mar 2025 20:06:15 +0300 Subject: [PATCH 61/73] core: add modular `network_proxy` support (#6399) * core: add modular `network_proxy` support Co-authored-by: @ImpostorKeanu Signed-off-by: Mohammed Al Sahaf * move modules around Signed-off-by: Mohammed Al Sahaf * add caddyfile implementation Signed-off-by: Mohammed Al Sahaf * address feedbcak * Apply suggestions from code review Co-authored-by: Francis Lavoie * adapt ForwardProxyURL to use the NetworkProxyRaw Signed-off-by: Mohammed Al Sahaf * remove redundant `url` in log Co-authored-by: Matt Holt * code review Signed-off-by: Mohammed Al Sahaf * remove `.source` from the module ID Signed-off-by: Mohammed Al Sahaf --------- Signed-off-by: Mohammed Al Sahaf Co-authored-by: Francis Lavoie Co-authored-by: Matt Holt --- ...proxy_http_transport_forward_proxy_url.txt | 41 +++++ ...everse_proxy_http_transport_none_proxy.txt | 40 +++++ ...reverse_proxy_http_transport_url_proxy.txt | 41 +++++ modules.go | 10 ++ modules/caddyhttp/reverseproxy/caddyfile.go | 24 ++- .../caddyhttp/reverseproxy/httptransport.go | 40 ++++- modules/caddytls/acmeissuer.go | 19 ++- modules/internal/network/networkproxy.go | 144 ++++++++++++++++++ 8 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt create mode 100644 modules/internal/network/networkproxy.go diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt new file mode 100644 index 000000000..9fc445283 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt @@ -0,0 +1,41 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + forward_proxy_url http://localhost:8080 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "network_proxy": { + "from": "url", + "url": "http://localhost:8080" + }, + "protocol": "http" + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt new file mode 100644 index 000000000..3805448d9 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt @@ -0,0 +1,40 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + network_proxy none + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "network_proxy": { + "from": "none" + }, + "protocol": "http" + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt new file mode 100644 index 000000000..9397458e9 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt @@ -0,0 +1,41 @@ +:8884 +reverse_proxy 127.0.0.1:65535 { + transport http { + network_proxy url http://localhost:8080 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "network_proxy": { + "from": "url", + "url": "http://localhost:8080" + }, + "protocol": "http" + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/modules.go b/modules.go index 470c25e37..37b56a988 100644 --- a/modules.go +++ b/modules.go @@ -18,6 +18,8 @@ import ( "bytes" "encoding/json" "fmt" + "net/http" + "net/url" "reflect" "sort" "strings" @@ -360,6 +362,14 @@ func isModuleMapType(typ reflect.Type) bool { isJSONRawMessage(typ.Elem()) } +// ProxyFuncProducer is implemented by modules which produce a +// function that returns a URL to use as network proxy. Modules +// in the namespace `caddy.network_proxy` must implement this +// interface. +type ProxyFuncProducer interface { + ProxyFunc() func(*http.Request) (*url.URL, error) +} + var ( modules = make(map[string]ModuleInfo) modulesMu sync.RWMutex diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index ab1dcdd02..d0947197a 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -33,6 +33,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/caddyserver/caddy/v2/modules/internal/network" ) func init() { @@ -979,7 +980,9 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // read_buffer // write_buffer // max_response_header -// forward_proxy_url +// network_proxy { +// ... +// } // dial_timeout // dial_fallback_delay // response_header_timeout @@ -990,6 +993,9 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // tls_insecure_skip_verify // tls_timeout // tls_trusted_ca_certs +// tls_trust_pool { +// ... +// } // tls_server_name // tls_renegotiation // tls_except_ports @@ -1068,10 +1074,24 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } case "forward_proxy_url": + caddy.Log().Warn("The 'forward_proxy_url' field is deprecated. Use 'network_proxy ' instead.") if !d.NextArg() { return d.ArgErr() } - h.ForwardProxyURL = d.Val() + u := network.ProxyFromURL{URL: d.Val()} + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil) + + case "network_proxy": + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "caddy.network_proxy." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(unm, "from", modStem, nil) case "dial_timeout": if !d.NextArg() { diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 910033ca1..92fe9ab7c 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -24,7 +24,6 @@ import ( weakrand "math/rand" "net" "net/http" - "net/url" "os" "reflect" "slices" @@ -38,8 +37,10 @@ import ( "golang.org/x/net/http2" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" + "github.com/caddyserver/caddy/v2/modules/internal/network" ) func init() { @@ -90,6 +91,7 @@ type HTTPTransport struct { // forward_proxy_url -> upstream // // Default: http.ProxyFromEnvironment + // DEPRECATED: Use NetworkProxyRaw|`network_proxy` instead. Subject to removal. ForwardProxyURL string `json:"forward_proxy_url,omitempty"` // How long to wait before timing out trying to connect to @@ -141,6 +143,22 @@ type HTTPTransport struct { // The pre-configured underlying HTTP transport. Transport *http.Transport `json:"-"` + // The module that provides the network (forward) proxy + // URL that the HTTP transport will use to proxy + // requests to the upstream. See [http.Transport.Proxy](https://pkg.go.dev/net/http#Transport.Proxy) + // for information regarding supported protocols. + // + // Providing a value to this parameter results in requests + // flowing through the reverse_proxy in the following way: + // + // User Agent -> + // reverse_proxy -> + // [proxy provided by the module] -> upstream + // + // If nil, defaults to reading the `HTTP_PROXY`, + // `HTTPS_PROXY`, and `NO_PROXY` environment variables. + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` + h2cTransport *http2.Transport h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) } @@ -328,16 +346,22 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } // negotiate any HTTP/SOCKS proxy for the HTTP transport - var proxy func(*http.Request) (*url.URL, error) + proxy := http.ProxyFromEnvironment if h.ForwardProxyURL != "" { - pUrl, err := url.Parse(h.ForwardProxyURL) + caddyCtx.Logger().Warn("forward_proxy_url is deprecated; use network_proxy instead") + u := network.ProxyFromURL{URL: h.ForwardProxyURL} + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil) + } + if len(h.NetworkProxyRaw) != 0 { + proxyMod, err := caddyCtx.LoadModule(h, "ForwardProxyRaw") if err != nil { - return nil, fmt.Errorf("failed to parse transport proxy url: %v", err) + return nil, fmt.Errorf("failed to load network_proxy module: %v", err) + } + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok { + proxy = m.ProxyFunc() + } else { + return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") } - caddyCtx.Logger().Info("setting transport proxy url", zap.String("url", h.ForwardProxyURL)) - proxy = http.ProxyURL(pUrl) - } else { - proxy = http.ProxyFromEnvironment } rt := &http.Transport{ diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index a07a34c10..bf2ebeacc 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -106,6 +106,9 @@ type ACMEIssuer struct { // be used. EXPERIMENTAL: Subject to change. CertificateLifetime caddy.Duration `json:"certificate_lifetime,omitempty"` + // Forward proxy module + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` + rootPool *x509.CertPool logger *zap.Logger @@ -194,7 +197,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { } var err error - iss.template, err = iss.makeIssuerTemplate() + iss.template, err = iss.makeIssuerTemplate(ctx) if err != nil { return err } @@ -202,7 +205,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { return nil } -func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { +func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssuer, error) { template := certmagic.ACMEIssuer{ CA: iss.CA, TestCA: iss.TestCA, @@ -216,6 +219,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { Logger: iss.logger, } + if len(iss.NetworkProxyRaw) != 0 { + proxyMod, err := ctx.LoadModule(iss, "ForwardProxyRaw") + if err != nil { + return template, fmt.Errorf("failed to load network_proxy module: %v", err) + } + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok { + template.HTTPProxy = m.ProxyFunc() + } else { + return template, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") + } + } + if iss.Challenges != nil { if iss.Challenges.HTTP != nil { template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled diff --git a/modules/internal/network/networkproxy.go b/modules/internal/network/networkproxy.go new file mode 100644 index 000000000..f9deeb43a --- /dev/null +++ b/modules/internal/network/networkproxy.go @@ -0,0 +1,144 @@ +package network + +import ( + "errors" + "net/http" + "net/url" + "strings" + + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func init() { + caddy.RegisterModule(ProxyFromURL{}) + caddy.RegisterModule(ProxyFromNone{}) +} + +// The "url" proxy source uses the defined URL as the proxy +type ProxyFromURL struct { + URL string `json:"url"` + + ctx caddy.Context + logger *zap.Logger +} + +// CaddyModule implements Module. +func (p ProxyFromURL) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.network_proxy.url", + New: func() caddy.Module { + return &ProxyFromURL{} + }, + } +} + +func (p *ProxyFromURL) Provision(ctx caddy.Context) error { + p.ctx = ctx + p.logger = ctx.Logger() + return nil +} + +// Validate implements Validator. +func (p ProxyFromURL) Validate() error { + if _, err := url.Parse(p.URL); err != nil { + return err + } + return nil +} + +// ProxyFunc implements ProxyFuncProducer. +func (p ProxyFromURL) ProxyFunc() func(*http.Request) (*url.URL, error) { + if strings.Contains(p.URL, "{") && strings.Contains(p.URL, "}") { + // courtesy of @ImpostorKeanu: https://github.com/caddyserver/caddy/pull/6397 + return func(r *http.Request) (*url.URL, error) { + // retrieve the replacer from context. + repl, ok := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + err := errors.New("failed to obtain replacer from request") + p.logger.Error(err.Error()) + return nil, err + } + + // apply placeholders to the value + // note: h.ForwardProxyURL should never be empty at this point + s := repl.ReplaceAll(p.URL, "") + if s == "" { + p.logger.Error("network_proxy URL was empty after applying placeholders", + zap.String("initial_value", p.URL), + zap.String("final_value", s), + zap.String("hint", "check for invalid placeholders")) + return nil, errors.New("empty value for network_proxy URL") + } + + // parse the url + pUrl, err := url.Parse(s) + if err != nil { + p.logger.Warn("failed to derive transport proxy from network_proxy URL") + pUrl = nil + } else if pUrl.Host == "" || strings.Split("", pUrl.Host)[0] == ":" { + // url.Parse does not return an error on these values: + // + // - http://:80 + // - pUrl.Host == ":80" + // - /some/path + // - pUrl.Host == "" + // + // Super edge cases, but humans are human. + err = errors.New("supplied network_proxy URL is missing a host value") + pUrl = nil + } else { + p.logger.Debug("setting transport proxy url", zap.String("url", s)) + } + + return pUrl, err + } + } + return func(r *http.Request) (*url.URL, error) { + return url.Parse(p.URL) + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (p *ProxyFromURL) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() + d.Next() + p.URL = d.Val() + return nil +} + +// The "none" proxy source module disables the use of network proxy. +type ProxyFromNone struct{} + +func (p ProxyFromNone) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.network_proxy.none", + New: func() caddy.Module { + return &ProxyFromNone{} + }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (p ProxyFromNone) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + +// ProxyFunc implements ProxyFuncProducer. +func (p ProxyFromNone) ProxyFunc() func(*http.Request) (*url.URL, error) { + return nil +} + +var ( + _ caddy.Module = ProxyFromURL{} + _ caddy.Provisioner = (*ProxyFromURL)(nil) + _ caddy.Validator = ProxyFromURL{} + _ caddy.ProxyFuncProducer = ProxyFromURL{} + _ caddyfile.Unmarshaler = (*ProxyFromURL)(nil) + + _ caddy.Module = ProxyFromNone{} + _ caddy.ProxyFuncProducer = ProxyFromNone{} + _ caddyfile.Unmarshaler = ProxyFromNone{} +) From 782a3c7ac60c82311fe9fb8889dd843dfe26c0bc Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 24 Mar 2025 09:55:26 -0600 Subject: [PATCH 62/73] caddytls: Don't publish HTTPS record for CNAME'd domain (fix #6922) --- modules/caddytls/ech.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index a192c3390..142cf48d6 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -630,6 +630,7 @@ func (dnsPub ECHDNSPublisher) PublisherKey() string { func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, configListBin []byte) error { nameservers := certmagic.RecursiveNameservers(nil) // TODO: we could make resolvers configurable +nextName: for _, domain := range innerNames { zone, err := certmagic.FindZoneByFQDN(ctx, dnsPub.logger, domain, nameservers) if err != nil { @@ -660,6 +661,14 @@ func (dnsPub *ECHDNSPublisher) PublishECHConfigList(ctx context.Context, innerNa var nameHasExistingRecord bool for _, rec := range recs { if rec.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" { + 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)) + continue nextName + } nameHasExistingRecord = true if rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { httpsRec = rec From 86c620fb4e7bfad5888832c491147af53fd5390a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 24 Mar 2025 16:16:11 -0600 Subject: [PATCH 63/73] go.mod: Minor dependency upgrades --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 4f701c778..21f5a1138 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.0 + github.com/caddyserver/certmagic v0.22.1 github.com/caddyserver/zerossl v0.1.3 github.com/cloudflare/circl v1.6.0 github.com/dustin/go-humanize v1.0.1 @@ -17,7 +17,7 @@ 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.0 + github.com/mholt/acmez/v3 v3.1.1 github.com/prometheus/client_golang v1.19.1 github.com/quic-go/quic-go v0.50.1 github.com/smallstep/certificates v0.26.1 diff --git a/go.sum b/go.sum index 990096efd..62717e087 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.0 h1:hi2skv2jouUw9uQUEyYSTTmqPZPHgf61dOANSIVCLOw= -github.com/caddyserver/certmagic v0.22.0/go.mod h1:Vc0msarAPhOagbDc/SU6M2zbzdwVuZ0lkTh2EqtH4vs= +github.com/caddyserver/certmagic v0.22.1 h1:FTI1H+N7UrVWx1mVW7Am0JrOtTNgJO/uOsIhjtDZM60= +github.com/caddyserver/certmagic v0.22.1/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs= 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= @@ -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.0 h1:RlOx2SSZ8dIAM5GfkMe8TdaxjjkiHTGorlMUt8GeMzg= -github.com/mholt/acmez/v3 v3.1.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +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/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= From 7672b7848f196e1baa859bcc2cf6610546097947 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 24 Mar 2025 20:51:05 -0600 Subject: [PATCH 64/73] go.mod: Upgrade CertMagic Hotfix for wildcard certs (regression in beta 3) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 21f5a1138..6c5894540 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.1 + github.com/caddyserver/certmagic v0.22.2 github.com/caddyserver/zerossl v0.1.3 github.com/cloudflare/circl v1.6.0 github.com/dustin/go-humanize v1.0.1 diff --git a/go.sum b/go.sum index 62717e087..e2c85355b 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.1 h1:FTI1H+N7UrVWx1mVW7Am0JrOtTNgJO/uOsIhjtDZM60= -github.com/caddyserver/certmagic v0.22.1/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs= +github.com/caddyserver/certmagic v0.22.2 h1:qzZURXlrxwR5m25/jpvVeEyJHeJJMvAwe5zlMufOTQk= +github.com/caddyserver/certmagic v0.22.2/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs= 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= From ea77a9ab67d8c04f513adaf0a1c648c738e25922 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 25 Mar 2025 16:24:16 -0600 Subject: [PATCH 65/73] caddytls: Temporarily treat "" and "@" as equivalent for DNS publication Fixes https://github.com/caddyserver/caddy/issues/6895#issuecomment-2750111096 --- modules/caddytls/ech.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 142cf48d6..f7b8db995 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -660,7 +660,8 @@ nextName: var httpsRec libdns.Record var nameHasExistingRecord bool for _, rec := range recs { - if rec.Name == relName { + // 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 == "@") { // 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" { From 5a6b2f8d1d4633622b551357f3cc9d27ec669d02 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Sat, 29 Mar 2025 08:15:43 -0600 Subject: [PATCH 66/73] events: Refactor; move Event into core, so core can emit events (#6930) * events: Refactor; move Event into core, so core can emit events Requires some slight trickery to invert dependencies. We can't have the caddy package import the caddyevents package, because caddyevents imports caddy. Interface to the rescue! Also add two new events, experimentally: started, and stopping. At the request of a sponsor. Also rename "Filesystems" to "FileSystems" to match Go convention (unrelated to events, was just bugging me when I noticed it). * Coupla bug fixes * lol whoops --- caddy.go | 104 +++++++++++++++++- context.go | 32 +++++- internal/filesystems/map.go | 32 +++--- modules/caddyevents/app.go | 110 ++++--------------- modules/caddyfs/filesystem.go | 4 +- modules/caddyhttp/fileserver/matcher.go | 2 +- modules/caddyhttp/fileserver/matcher_test.go | 6 +- modules/caddyhttp/fileserver/staticfiles.go | 2 +- 8 files changed, 170 insertions(+), 122 deletions(-) diff --git a/caddy.go b/caddy.go index 758b0b2f6..d6a2ae0b3 100644 --- a/caddy.go +++ b/caddy.go @@ -81,13 +81,14 @@ type Config struct { // associated value. AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="` - apps map[string]App - storage certmagic.Storage + apps map[string]App + storage certmagic.Storage + eventEmitter eventEmitter cancelFunc context.CancelFunc - // filesystems is a dict of filesystems that will later be loaded from and added to. - filesystems FileSystems + // fileSystems is a dict of fileSystems that will later be loaded from and added to. + fileSystems FileSystems } // App is a thing that Caddy runs. @@ -442,6 +443,10 @@ func run(newCfg *Config, start bool) (Context, error) { } globalMetrics.configSuccess.Set(1) globalMetrics.configSuccessTime.SetToCurrentTime() + + // TODO: This event is experimental and subject to change. + ctx.emitEvent("started", nil) + // now that the user's config is running, finish setting up anything else, // such as remote admin endpoint, config loader, etc. return ctx, finishSettingUp(ctx, ctx.cfg) @@ -509,7 +514,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) } // create the new filesystem map - newCfg.filesystems = &filesystems.FilesystemMap{} + newCfg.fileSystems = &filesystems.FileSystemMap{} // prepare the new config for use newCfg.apps = make(map[string]App) @@ -696,6 +701,9 @@ func unsyncedStop(ctx Context) { return } + // TODO: This event is experimental and subject to change. + ctx.emitEvent("stopping", nil) + // stop each app for name, a := range ctx.cfg.apps { err := a.Stop() @@ -1038,6 +1046,92 @@ func Version() (simple, full string) { return } +// Event represents something that has happened or is happening. +// An Event value is not synchronized, so it should be copied if +// being used in goroutines. +// +// EXPERIMENTAL: Events are subject to change. +type Event struct { + // If non-nil, the event has been aborted, meaning + // propagation has stopped to other handlers and + // the code should stop what it was doing. Emitters + // may choose to use this as a signal to adjust their + // code path appropriately. + Aborted error + + // The data associated with the event. Usually the + // original emitter will be the only one to set or + // change these values, but the field is exported + // so handlers can have full access if needed. + // However, this map is not synchronized, so + // handlers must not use this map directly in new + // goroutines; instead, copy the map to use it in a + // goroutine. Data may be nil. + Data map[string]any + + id uuid.UUID + ts time.Time + name string + origin Module +} + +// NewEvent creates a new event, but does not emit the event. To emit an +// event, call Emit() on the current instance of the caddyevents app insteaad. +// +// EXPERIMENTAL: Subject to change. +func NewEvent(ctx Context, name string, data map[string]any) (Event, error) { + id, err := uuid.NewRandom() + if err != nil { + return Event{}, fmt.Errorf("generating new event ID: %v", err) + } + name = strings.ToLower(name) + return Event{ + Data: data, + id: id, + ts: time.Now(), + name: name, + origin: ctx.Module(), + }, nil +} + +func (e Event) ID() uuid.UUID { return e.id } +func (e Event) Timestamp() time.Time { return e.ts } +func (e Event) Name() string { return e.name } +func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event. + +// CloudEvent exports event e as a structure that, when +// serialized as JSON, is compatible with the +// CloudEvents spec. +func (e Event) CloudEvent() CloudEvent { + dataJSON, _ := json.Marshal(e.Data) + return CloudEvent{ + ID: e.id.String(), + Source: e.origin.CaddyModule().String(), + SpecVersion: "1.0", + Type: e.name, + Time: e.ts, + DataContentType: "application/json", + Data: dataJSON, + } +} + +// CloudEvent is a JSON-serializable structure that +// is compatible with the CloudEvents specification. +// See https://cloudevents.io. +// EXPERIMENTAL: Subject to change. +type CloudEvent struct { + ID string `json:"id"` + Source string `json:"source"` + SpecVersion string `json:"specversion"` + Type string `json:"type"` + Time time.Time `json:"time"` + DataContentType string `json:"datacontenttype,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +// ErrEventAborted cancels an event. +var ErrEventAborted = errors.New("event aborted") + // ActiveContext returns the currently-active context. // This function is experimental and might be changed // or removed in the future. diff --git a/context.go b/context.go index 94623df72..a65814f03 100644 --- a/context.go +++ b/context.go @@ -91,14 +91,14 @@ func (ctx *Context) OnCancel(f func()) { ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) } -// Filesystems returns a ref to the FilesystemMap. +// FileSystems returns a ref to the FilesystemMap. // EXPERIMENTAL: This API is subject to change. -func (ctx *Context) Filesystems() FileSystems { +func (ctx *Context) FileSystems() FileSystems { // if no config is loaded, we use a default filesystemmap, which includes the osfs if ctx.cfg == nil { - return &filesystems.FilesystemMap{} + return &filesystems.FileSystemMap{} } - return ctx.cfg.filesystems + return ctx.cfg.fileSystems } // Returns the active metrics registry for the context @@ -277,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) return result, nil } +// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured. +func (ctx Context) emitEvent(name string, data map[string]any) Event { + if ctx.cfg == nil || ctx.cfg.eventEmitter == nil { + return Event{} + } + return ctx.cfg.eventEmitter.Emit(ctx, name, data) +} + // loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any. // Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module // name) or as a regular map (key is not the module name, and module name is defined inline). @@ -429,6 +437,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val) + // if the loaded module happens to be an app that can emit events, store it so the + // core can have access to emit events without an import cycle + if ee, ok := val.(eventEmitter); ok { + if _, ok := ee.(App); ok { + ctx.cfg.eventEmitter = ee + } + } + return val, nil } @@ -600,3 +616,11 @@ func (ctx *Context) WithValue(key, value any) Context { exitFuncs: ctx.exitFuncs, } } + +// eventEmitter is a small interface that inverts dependencies for +// the caddyevents package, so the core can emit events without an +// import cycle (i.e. the caddy package doesn't have to import +// the caddyevents package, which imports the caddy package). +type eventEmitter interface { + Emit(ctx Context, eventName string, data map[string]any) Event +} diff --git a/internal/filesystems/map.go b/internal/filesystems/map.go index e795ed1fe..3ecb34e40 100644 --- a/internal/filesystems/map.go +++ b/internal/filesystems/map.go @@ -7,10 +7,10 @@ import ( ) const ( - DefaultFilesystemKey = "default" + DefaultFileSystemKey = "default" ) -var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}} +var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}} // wrapperFs exists so can easily add to wrapperFs down the line type wrapperFs struct { @@ -18,24 +18,24 @@ type wrapperFs struct { fs.FS } -// FilesystemMap stores a map of filesystems +// FileSystemMap stores a map of filesystems // the empty key will be overwritten to be the default key // it includes a default filesystem, based off the os fs -type FilesystemMap struct { +type FileSystemMap struct { m sync.Map } // note that the first invocation of key cannot be called in a racy context. -func (f *FilesystemMap) key(k string) string { +func (f *FileSystemMap) key(k string) string { if k == "" { - k = DefaultFilesystemKey + k = DefaultFileSystemKey } return k } // Register will add the filesystem with key to later be retrieved // A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil -func (f *FilesystemMap) Register(k string, v fs.FS) { +func (f *FileSystemMap) Register(k string, v fs.FS) { k = f.key(k) if v == nil { f.Unregister(k) @@ -47,23 +47,23 @@ func (f *FilesystemMap) Register(k string, v fs.FS) { // Unregister will remove the filesystem with key from the filesystem map // if the key is the default key, it will set the default to the osFS instead of deleting it // modules should call this on cleanup to be safe -func (f *FilesystemMap) Unregister(k string) { +func (f *FileSystemMap) Unregister(k string) { k = f.key(k) - if k == DefaultFilesystemKey { - f.m.Store(k, DefaultFilesystem) + if k == DefaultFileSystemKey { + f.m.Store(k, DefaultFileSystem) } else { f.m.Delete(k) } } // Get will get a filesystem with a given key -func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) { +func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) { k = f.key(k) c, ok := f.m.Load(strings.TrimSpace(k)) if !ok { - if k == DefaultFilesystemKey { - f.m.Store(k, DefaultFilesystem) - return DefaultFilesystem, true + if k == DefaultFileSystemKey { + f.m.Store(k, DefaultFileSystem) + return DefaultFileSystem, true } return nil, ok } @@ -71,7 +71,7 @@ func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) { } // Default will get the default filesystem in the filesystem map -func (f *FilesystemMap) Default() fs.FS { - val, _ := f.Get(DefaultFilesystemKey) +func (f *FileSystemMap) Default() fs.FS { + val, _ := f.Get(DefaultFileSystemKey) return val } diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index e78b00f8c..9fc8fa8ed 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -20,9 +20,7 @@ import ( "errors" "fmt" "strings" - "time" - "github.com/google/uuid" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" @@ -206,27 +204,26 @@ func (app *App) On(eventName string, handler Handler) error { // // Note that the data map is not copied, for efficiency. After Emit() is called, the // data passed in should not be changed in other goroutines. -func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event { +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event { logger := app.logger.With(zap.String("name", eventName)) - id, err := uuid.NewRandom() + e, err := caddy.NewEvent(ctx, eventName, data) if err != nil { - logger.Error("failed generating new event ID", zap.Error(err)) + logger.Error("failed to create event", zap.Error(err)) } - eventName = strings.ToLower(eventName) - - e := Event{ - Data: data, - id: id, - ts: time.Now(), - name: eventName, - origin: ctx.Module(), + var originModule caddy.ModuleInfo + var originModuleID caddy.ModuleID + var originModuleName string + if origin := e.Origin(); origin != nil { + originModule = origin.CaddyModule() + originModuleID = originModule.ID + originModuleName = originModule.String() } logger = logger.With( - zap.String("id", e.id.String()), - zap.String("origin", e.origin.CaddyModule().String())) + zap.String("id", e.ID().String()), + zap.String("origin", originModuleName)) // add event info to replacer, make sure it's in the context repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) @@ -239,15 +236,15 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E case "event": return e, true case "event.id": - return e.id, true + return e.ID(), true case "event.name": - return e.name, true + return e.Name(), true case "event.time": - return e.ts, true + return e.Timestamp(), true case "event.time_unix": - return e.ts.UnixMilli(), true + return e.Timestamp().UnixMilli(), true case "event.module": - return e.origin.CaddyModule().ID, true + return originModuleID, true case "event.data": return e.Data, true } @@ -269,7 +266,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E // invoke handlers bound to the event by name and also all events; this for loop // iterates twice at most: once for the event name, once for "" (all events) for { - moduleID := e.origin.CaddyModule().ID + moduleID := originModuleID // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "") for { @@ -292,7 +289,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E zap.Any("handler", handler)) if err := handler.Handle(ctx, e); err != nil { - aborted := errors.Is(err, ErrAborted) + aborted := errors.Is(err, caddy.ErrEventAborted) logger.Error("handler error", zap.Error(err), @@ -326,76 +323,9 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E return e } -// Event represents something that has happened or is happening. -// An Event value is not synchronized, so it should be copied if -// being used in goroutines. -// -// EXPERIMENTAL: As with the rest of this package, events are -// subject to change. -type Event struct { - // If non-nil, the event has been aborted, meaning - // propagation has stopped to other handlers and - // the code should stop what it was doing. Emitters - // may choose to use this as a signal to adjust their - // code path appropriately. - Aborted error - - // The data associated with the event. Usually the - // original emitter will be the only one to set or - // change these values, but the field is exported - // so handlers can have full access if needed. - // However, this map is not synchronized, so - // handlers must not use this map directly in new - // goroutines; instead, copy the map to use it in a - // goroutine. - Data map[string]any - - id uuid.UUID - ts time.Time - name string - origin caddy.Module -} - -func (e Event) ID() uuid.UUID { return e.id } -func (e Event) Timestamp() time.Time { return e.ts } -func (e Event) Name() string { return e.name } -func (e Event) Origin() caddy.Module { return e.origin } - -// CloudEvent exports event e as a structure that, when -// serialized as JSON, is compatible with the -// CloudEvents spec. -func (e Event) CloudEvent() CloudEvent { - dataJSON, _ := json.Marshal(e.Data) - return CloudEvent{ - ID: e.id.String(), - Source: e.origin.CaddyModule().String(), - SpecVersion: "1.0", - Type: e.name, - Time: e.ts, - DataContentType: "application/json", - Data: dataJSON, - } -} - -// CloudEvent is a JSON-serializable structure that -// is compatible with the CloudEvents specification. -// See https://cloudevents.io. -type CloudEvent struct { - ID string `json:"id"` - Source string `json:"source"` - SpecVersion string `json:"specversion"` - Type string `json:"type"` - Time time.Time `json:"time"` - DataContentType string `json:"datacontenttype,omitempty"` - Data json.RawMessage `json:"data,omitempty"` -} - -// ErrAborted cancels an event. -var ErrAborted = errors.New("event aborted") - // Handler is a type that can handle events. type Handler interface { - Handle(context.Context, Event) error + Handle(context.Context, caddy.Event) error } // Interface guards diff --git a/modules/caddyfs/filesystem.go b/modules/caddyfs/filesystem.go index b2fdcf7a2..2ec43079a 100644 --- a/modules/caddyfs/filesystem.go +++ b/modules/caddyfs/filesystem.go @@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error { } // register that module ctx.Logger().Debug("registering fs", zap.String("fs", f.Key)) - ctx.Filesystems().Register(f.Key, f.fileSystem) + ctx.FileSystems().Register(f.Key, f.fileSystem) // remember to unregister the module when we are done xs.defers = append(xs.defers, func() { ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key)) - ctx.Filesystems().Unregister(f.Key) + ctx.FileSystems().Unregister(f.Key) }) } return nil diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 2bc665d4f..b5b4c9f0f 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -274,7 +274,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander { func (m *MatchFile) Provision(ctx caddy.Context) error { m.logger = ctx.Logger() - m.fsmap = ctx.Filesystems() + m.fsmap = ctx.FileSystems() if m.Root == "" { m.Root = "{http.vars.root}" diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index b6697b9d8..f0ec4b392 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -117,7 +117,7 @@ func TestFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - fsmap: &filesystems.FilesystemMap{}, + fsmap: &filesystems.FileSystemMap{}, Root: "./testdata", TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, } @@ -229,7 +229,7 @@ func TestPHPFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - fsmap: &filesystems.FilesystemMap{}, + fsmap: &filesystems.FileSystemMap{}, Root: "./testdata", TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, SplitPath: []string{".php"}, @@ -273,7 +273,7 @@ func TestPHPFileMatcher(t *testing.T) { func TestFirstSplit(t *testing.T) { m := MatchFile{ SplitPath: []string{".php"}, - fsmap: &filesystems.FilesystemMap{}, + fsmap: &filesystems.FileSystemMap{}, } actual, remainder := m.firstSplit("index.PHP/somewhere") expected := "index.PHP" diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 2b0caecfc..1072d1878 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -186,7 +186,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo { func (fsrv *FileServer) Provision(ctx caddy.Context) error { fsrv.logger = ctx.Logger() - fsrv.fsmap = ctx.Filesystems() + fsrv.fsmap = ctx.FileSystems() if fsrv.FileSystem == "" { fsrv.FileSystem = "{http.vars.fs}" From 9becf61a9f5bafb88a15823ce80c1325d3a30a4f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 7 Apr 2025 12:43:11 -0600 Subject: [PATCH 67/73] go.mod: Upgrade to libdns 1.0 beta APIs (requires upgraded DNS providers) This is the only way we can properly, reliably support ECH. --- go.mod | 4 +- go.sum | 8 +- modules/caddytls/ech.go | 213 ++++------------------------------- modules/caddytls/ech_test.go | 129 --------------------- 4 files changed, 25 insertions(+), 329 deletions(-) delete mode 100644 modules/caddytls/ech_test.go diff --git a/go.mod b/go.mod index 6c5894540..614dce032 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.22.3-0.20250407182622-b9399eadfbe7 github.com/caddyserver/zerossl v0.1.3 github.com/cloudflare/circl v1.6.0 github.com/dustin/go-humanize v1.0.1 @@ -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..676849128 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.22.3-0.20250407182622-b9399eadfbe7 h1:dZCbkHTYh2ulXpmUK4ZrYWMCTa7x4/F3w52RGQfYoIY= +github.com/caddyserver/certmagic v0.22.3-0.20250407182622-b9399eadfbe7/go.mod h1:r8ZK7Y8zaUU3j5g+oTiluLtQX8fwWI6MamcRINlSsJY= 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= diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index f7b8db995..fe0ba93b7 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -641,10 +641,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 +653,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 +687,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 - Target: ".", - Value: params.String(), TTL: 1 * time.Minute, // TODO: for testing only + Priority: 2, // allows a manual override with priority 1 + Target: ".", + Params: params, }, }) if err != nil { @@ -952,172 +943,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 b722d2fbf..000000000 --- a/modules/caddytls/ech_test.go +++ /dev/null @@ -1,129 +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 - } - } -} From b06a9496d130cb06466156d53138a9691342e5a2 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 8 Apr 2025 13:59:02 -0600 Subject: [PATCH 68/73] caddyhttp: Document side effect of HTTP/3 early data (close #6936) --- modules/caddyhttp/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index cbd168d31..317825ac5 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` From ce926b87ed606a414e93d768f8ff5c666c070347 Mon Sep 17 00:00:00 2001 From: riyueguang Date: Sat, 12 Apr 2025 12:24:17 +0800 Subject: [PATCH 69/73] chore: fix comment (#6950) Signed-off-by: riyueguang --- listeners.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listeners.go b/listeners.go index b22df77ba..f91b5f087 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) From def9db1f16debab582fdc6e1880bbbe9162b3f93 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Sun, 13 Apr 2025 11:19:32 +0800 Subject: [PATCH 70/73] Fix the incorrect parameter order (#6951) Signed-off-by: cuishuang --- listeners.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listeners.go b/listeners.go index f91b5f087..9e0057678 100644 --- a/listeners.go +++ b/listeners.go @@ -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") } From 6c38ae7381b3338b173c59706673d11783091dee Mon Sep 17 00:00:00 2001 From: Jesper Brix Rosenkilde Date: Tue, 15 Apr 2025 16:44:53 +0200 Subject: [PATCH 71/73] reverseproxy: Add valid Upstream to DialInfo in active health checks (#6949) Currently if we extract the DialInfo from a Request Context during an active health check, then the Upstream in the DialInfo is nil. This PR attempts to set the Upstream to a sensible value, based on wether or not the Upstream has been overriden in the active health check's config. --- modules/caddyhttp/reverseproxy/healthchecks.go | 18 +++++++++++++++--- modules/caddyhttp/reverseproxy/hosts.go | 4 +--- modules/caddyhttp/reverseproxy/reverseproxy.go | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index f0ffee5b8..f018f40ca 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 0a676e431..300003f2b 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/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 5bdafa070..f07d3daeb 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) } From f297bc0a04dcab6c2585b47f3672d045c4f6b54b Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 15 Apr 2025 14:20:22 -0600 Subject: [PATCH 72/73] admin: Remove host checking for UDS (close #6832) The consensus is that host enforcement on unix sockets is ineffective, frustrating, and confusing. (Unix sockets have their own OS-level permissions system.) --- admin.go | 79 +++++++++++++++++++++++++-------------------------- admin_test.go | 15 ++++------ 2 files changed, 44 insertions(+), 50 deletions(-) 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) } }) } From 137711ae3e2d9aa48d7c48dba5ca176af628f073 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 15 Apr 2025 15:08:12 -0600 Subject: [PATCH 73/73] go.mod: Upgrade acmez and certmagic --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 614dce032..f272fb02a 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.3-0.20250407182622-b9399eadfbe7 + 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,7 +17,7 @@ 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/smallstep/certificates v0.26.1 diff --git a/go.sum b/go.sum index 676849128..807f6144c 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.3-0.20250407182622-b9399eadfbe7 h1:dZCbkHTYh2ulXpmUK4ZrYWMCTa7x4/F3w52RGQfYoIY= -github.com/caddyserver/certmagic v0.22.3-0.20250407182622-b9399eadfbe7/go.mod h1:r8ZK7Y8zaUU3j5g+oTiluLtQX8fwWI6MamcRINlSsJY= +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= @@ -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=