From 6c3df26b3c64c47541e45484f7d4b2f9a03d5894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20=22Pisco=22=20Fernandes?= Date: Thu, 7 Aug 2025 12:34:45 +0100 Subject: [PATCH 1/7] vuln: Fix GO-2025-3770 vulnerability --- go.mod | 2 +- go.sum | 4 +- vendor/github.com/go-chi/chi/v5/CHANGELOG.md | 14 ++++- .../github.com/go-chi/chi/v5/CONTRIBUTING.md | 12 ++--- vendor/github.com/go-chi/chi/v5/README.md | 15 ++++-- vendor/github.com/go-chi/chi/v5/SECURITY.md | 5 ++ vendor/github.com/go-chi/chi/v5/chi.go | 9 ++-- vendor/github.com/go-chi/chi/v5/context.go | 31 ++++++----- vendor/github.com/go-chi/chi/v5/mux.go | 54 +++++++++++++++---- vendor/github.com/go-chi/chi/v5/path_value.go | 21 ++++++++ .../go-chi/chi/v5/path_value_fallback.go | 19 +++++++ vendor/github.com/go-chi/chi/v5/tree.go | 6 +-- vendor/modules.txt | 4 +- 13 files changed, 148 insertions(+), 48 deletions(-) create mode 100644 vendor/github.com/go-chi/chi/v5/SECURITY.md create mode 100644 vendor/github.com/go-chi/chi/v5/path_value.go create mode 100644 vendor/github.com/go-chi/chi/v5/path_value_fallback.go diff --git a/go.mod b/go.mod index 2f9209b6..4138c30d 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/fortytw2/leaktest v1.3.0 github.com/fsnotify/fsnotify v1.4.9 github.com/getsentry/sentry-go v0.16.0 - github.com/go-chi/chi/v5 v5.0.10 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-jose/go-jose/v4 v4.1.0 github.com/gobwas/ws v1.2.1 diff --git a/go.sum b/go.sum index 4ea72fa8..8ff86248 100644 --- a/go.sum +++ b/go.sum @@ -58,8 +58,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= diff --git a/vendor/github.com/go-chi/chi/v5/CHANGELOG.md b/vendor/github.com/go-chi/chi/v5/CHANGELOG.md index f6eb7e6e..25b45b97 100644 --- a/vendor/github.com/go-chi/chi/v5/CHANGELOG.md +++ b/vendor/github.com/go-chi/chi/v5/CHANGELOG.md @@ -1,9 +1,19 @@ # Changelog +## v5.0.12 (2024-02-16) + +- History of changes: see https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12 + + +## v5.0.11 (2023-12-19) + +- History of changes: see https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11 + + ## v5.0.10 (2023-07-13) - Fixed small edge case in tests of v5.0.9 for older Go versions -- History of changes: see https://github.com/go-chi/chi/compare/v5.0.8...v5.0.10 +- History of changes: see https://github.com/go-chi/chi/compare/v5.0.9...v5.0.10 ## v5.0.9 (2023-07-13) @@ -306,7 +316,7 @@ Cheers all, happy coding! request-scoped values. We're very excited about the new context addition and are proud to introduce chi v2, a minimal and powerful routing package for building large HTTP services, with zero external dependencies. Chi focuses on idiomatic design and encourages the use of - stdlib HTTP handlers and middlwares. + stdlib HTTP handlers and middlewares. - chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` - chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` - chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, diff --git a/vendor/github.com/go-chi/chi/v5/CONTRIBUTING.md b/vendor/github.com/go-chi/chi/v5/CONTRIBUTING.md index c0ac2dfe..b4a6268d 100644 --- a/vendor/github.com/go-chi/chi/v5/CONTRIBUTING.md +++ b/vendor/github.com/go-chi/chi/v5/CONTRIBUTING.md @@ -14,7 +14,7 @@ A typical workflow is: -1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +1. [Fork the repository.][fork] 2. [Create a topic branch.][branch] 3. Add tests for your change. 4. Run `go test`. If your tests pass, return to the step 3. @@ -24,8 +24,8 @@ A typical workflow is: 8. [Submit a pull request.][pull-req] [go-install]: https://golang.org/doc/install -[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html -[fork]: https://help.github.com/articles/fork-a-repo -[branch]: http://learn.github.com/p/branching.html -[git-help]: https://guides.github.com -[pull-req]: https://help.github.com/articles/using-pull-requests +[fork]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo +[branch]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches +[git-help]: https://docs.github.com/en +[pull-req]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests + diff --git a/vendor/github.com/go-chi/chi/v5/README.md b/vendor/github.com/go-chi/chi/v5/README.md index 718e373f..c58a0e20 100644 --- a/vendor/github.com/go-chi/chi/v5/README.md +++ b/vendor/github.com/go-chi/chi/v5/README.md @@ -20,7 +20,9 @@ and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! ## Install -`go get -u github.com/go-chi/chi/v5` +```sh +go get -u github.com/go-chi/chi/v5 +``` ## Features @@ -65,7 +67,7 @@ func main() { **REST Preview:** -Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +Here is a little preview of what routing looks like with chi. Also take a look at the generated routing docs in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). @@ -194,7 +196,7 @@ type Router interface { // path, with a fresh middleware stack for the inline-Router. Group(fn func(r Router)) Router - // Route mounts a sub-Router along a `pattern`` string. + // Route mounts a sub-Router along a `pattern` string. Route(pattern string, fn func(r Router)) Router // Mount attaches another http.Handler along ./pattern/* @@ -354,6 +356,7 @@ with `net/http` can be used with chi's mux. | [RouteHeaders] | Route handling for request headers | | [SetHeader] | Short-hand middleware to set a response header key/value | | [StripSlashes] | Strip slashes on routing paths | +| [Sunset] | Sunset set Deprecation/Sunset header to response | | [Throttle] | Puts a ceiling on the number of concurrent requests | | [Timeout] | Signals to the request context when the timeout deadline is reached | | [URLFormat] | Parse extension from url and put it on request context | @@ -380,6 +383,7 @@ with `net/http` can be used with chi's mux. [RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders [SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader [StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Sunset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Sunset [Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle [ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog [ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts @@ -467,7 +471,8 @@ how setting context on a request in Go works. * Carl Jackson for https://github.com/zenazn/goji * Parts of chi's thinking comes from goji, and chi's middleware package - sources from goji. + sources from [goji](https://github.com/zenazn/goji/tree/master/web/middleware). + * Please see goji's [LICENSE](https://github.com/zenazn/goji/blob/master/LICENSE) (MIT) * Armon Dadgar for https://github.com/armon/go-radix * Contributions: [@VojtechVitek](https://github.com/VojtechVitek) @@ -494,7 +499,7 @@ Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) Licensed under [MIT License](./LICENSE) -[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi/v5 [GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg [Travis]: https://travis-ci.org/go-chi/chi [Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/vendor/github.com/go-chi/chi/v5/SECURITY.md b/vendor/github.com/go-chi/chi/v5/SECURITY.md new file mode 100644 index 00000000..7e937f87 --- /dev/null +++ b/vendor/github.com/go-chi/chi/v5/SECURITY.md @@ -0,0 +1,5 @@ +# Reporting Security Issues + +We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. + +To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/go-chi/chi/security/advisories/new) tab. diff --git a/vendor/github.com/go-chi/chi/v5/chi.go b/vendor/github.com/go-chi/chi/v5/chi.go index a1691bbe..2b6ebd33 100644 --- a/vendor/github.com/go-chi/chi/v5/chi.go +++ b/vendor/github.com/go-chi/chi/v5/chi.go @@ -37,8 +37,7 @@ // // A placeholder with a name followed by a colon allows a regular // expression match, for example {number:\\d+}. The regular expression -// syntax is Go's normal regexp RE2 syntax, except that regular expressions -// including { or } are not supported, and / will never be +// syntax is Go's normal regexp RE2 syntax, except that / will never be // matched. An anonymous regexp pattern is allowed, using an empty string // before the colon in the placeholder, such as {:\\d+} // @@ -51,7 +50,7 @@ // "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" // "/user/{name}/info" matches "/user/jsmith/info" // "/page/*" matches "/page/intro/latest" -// "/page/{other}/index" also matches "/page/intro/latest" +// "/page/{other}/latest" also matches "/page/intro/latest" // "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" package chi @@ -127,6 +126,10 @@ type Routes interface { // the method/path - similar to routing a http request, but without // executing the handler thereafter. Match(rctx *Context, method, path string) bool + + // Find searches the routing tree for the pattern that matches + // the method/path. + Find(rctx *Context, method, path string) string } // Middlewares type is a slice of standard middleware handlers with methods diff --git a/vendor/github.com/go-chi/chi/v5/context.go b/vendor/github.com/go-chi/chi/v5/context.go index 88f8e221..aacf6eff 100644 --- a/vendor/github.com/go-chi/chi/v5/context.go +++ b/vendor/github.com/go-chi/chi/v5/context.go @@ -60,7 +60,7 @@ type Context struct { URLParams RouteParams // Route parameters matched for the current sub-router. It is - // intentionally unexported so it cant be tampered. + // intentionally unexported so it can't be tampered. routeParams RouteParams // The endpoint routing pattern that matched the request URI path @@ -74,9 +74,8 @@ type Context struct { // patterns across a stack of sub-routers. RoutePatterns []string - // methodNotAllowed hint - methodNotAllowed bool methodsAllowed []methodTyp // allowed methods in case of a 405 + methodNotAllowed bool } // Reset a routing context to its initial state. @@ -92,6 +91,7 @@ func (x *Context) Reset() { x.routeParams.Keys = x.routeParams.Keys[:0] x.routeParams.Values = x.routeParams.Values[:0] x.methodNotAllowed = false + x.methodsAllowed = x.methodsAllowed[:0] x.parentCtx = nil } @@ -109,22 +109,27 @@ func (x *Context) URLParam(key string) string { // RoutePattern builds the routing pattern string for the particular // request, at the particular point during routing. This means, the value // will change throughout the execution of a request in a router. That is -// why its advised to only use this value after calling the next handler. +// why it's advised to only use this value after calling the next handler. // // For example, // -// func Instrument(next http.Handler) http.Handler { -// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// next.ServeHTTP(w, r) -// routePattern := chi.RouteContext(r.Context()).RoutePattern() -// measure(w, r, routePattern) -// }) -// } +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } func (x *Context) RoutePattern() string { + if x == nil { + return "" + } routePattern := strings.Join(x.RoutePatterns, "") routePattern = replaceWildcards(routePattern) - routePattern = strings.TrimSuffix(routePattern, "//") - routePattern = strings.TrimSuffix(routePattern, "/") + if routePattern != "/" { + routePattern = strings.TrimSuffix(routePattern, "//") + routePattern = strings.TrimSuffix(routePattern, "/") + } return routePattern } diff --git a/vendor/github.com/go-chi/chi/v5/mux.go b/vendor/github.com/go-chi/chi/v5/mux.go index 977aa52d..f1266971 100644 --- a/vendor/github.com/go-chi/chi/v5/mux.go +++ b/vendor/github.com/go-chi/chi/v5/mux.go @@ -107,12 +107,22 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { // Handle adds the route `pattern` that matches any http method to // execute the `handler` http.Handler. func (mx *Mux) Handle(pattern string, handler http.Handler) { + if method, rest, found := strings.Cut(pattern, " "); found { + mx.Method(method, rest, handler) + return + } + mx.handle(mALL, pattern, handler) } // HandleFunc adds the route `pattern` that matches any http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + if method, rest, found := strings.Cut(pattern, " "); found { + mx.Method(method, rest, handlerFn) + return + } + mx.handle(mALL, pattern, handlerFn) } @@ -250,20 +260,19 @@ func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { return im } -// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// Group creates a new inline-Mux with a copy of middleware stack. It's useful // for a group of handlers along the same routing path that use an additional // set of middlewares. See _examples/. func (mx *Mux) Group(fn func(r Router)) Router { - im := mx.With().(*Mux) + im := mx.With() if fn != nil { fn(im) } return im } -// Route creates a new Mux with a fresh middleware stack and mounts it -// along the `pattern` as a subrouter. Effectively, this is a short-hand -// call to Mount. See _examples/. +// Route creates a new Mux and mounts it along the `pattern` as a subrouter. +// Effectively, this is a short-hand call to Mount. See _examples/. func (mx *Mux) Route(pattern string, fn func(r Router)) Router { if fn == nil { panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) @@ -352,19 +361,40 @@ func (mx *Mux) Middlewares() Middlewares { // Note: the *Context state is updated during execution, so manage // the state carefully or make a NewRouteContext(). func (mx *Mux) Match(rctx *Context, method, path string) bool { + return mx.Find(rctx, method, path) != "" +} + +// Find searches the routing tree for the pattern that matches +// the method/path. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Find(rctx *Context, method, path string) string { m, ok := methodMap[method] if !ok { - return false + return "" } - node, _, h := mx.tree.FindRoute(rctx, m, path) + node, _, _ := mx.tree.FindRoute(rctx, m, path) + pattern := rctx.routePattern + + if node != nil { + if node.subroutes == nil { + e := node.endpoints[m] + return e.pattern + } - if node != nil && node.subroutes != nil { rctx.RoutePath = mx.nextRoutePath(rctx) - return node.subroutes.Match(rctx, method, rctx.RoutePath) + subPattern := node.subroutes.Find(rctx, method, rctx.RoutePath) + if subPattern == "" { + return "" + } + + pattern = strings.TrimSuffix(pattern, "/*") + pattern += subPattern } - return h != nil + return pattern } // NotFoundHandler returns the default Mux 404 responder whenever a route @@ -441,6 +471,10 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { // Find the route if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + if supportsPathValue { + setPathValue(rctx, r) + } + h.ServeHTTP(w, r) return } diff --git a/vendor/github.com/go-chi/chi/v5/path_value.go b/vendor/github.com/go-chi/chi/v5/path_value.go new file mode 100644 index 00000000..77c840f0 --- /dev/null +++ b/vendor/github.com/go-chi/chi/v5/path_value.go @@ -0,0 +1,21 @@ +//go:build go1.22 && !tinygo +// +build go1.22,!tinygo + + +package chi + +import "net/http" + +// supportsPathValue is true if the Go version is 1.22 and above. +// +// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`. +const supportsPathValue = true + +// setPathValue sets the path values in the Request value +// based on the provided request context. +func setPathValue(rctx *Context, r *http.Request) { + for i, key := range rctx.URLParams.Keys { + value := rctx.URLParams.Values[i] + r.SetPathValue(key, value) + } +} diff --git a/vendor/github.com/go-chi/chi/v5/path_value_fallback.go b/vendor/github.com/go-chi/chi/v5/path_value_fallback.go new file mode 100644 index 00000000..749a8520 --- /dev/null +++ b/vendor/github.com/go-chi/chi/v5/path_value_fallback.go @@ -0,0 +1,19 @@ +//go:build !go1.22 || tinygo +// +build !go1.22 tinygo + +package chi + +import "net/http" + +// supportsPathValue is true if the Go version is 1.22 and above. +// +// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`. +const supportsPathValue = false + +// setPathValue sets the path values in the Request value +// based on the provided request context. +// +// setPathValue is only supported in Go 1.22 and above so +// this is just a blank function so that it compiles. +func setPathValue(rctx *Context, r *http.Request) { +} diff --git a/vendor/github.com/go-chi/chi/v5/tree.go b/vendor/github.com/go-chi/chi/v5/tree.go index c7d3bc57..85fcfdbb 100644 --- a/vendor/github.com/go-chi/chi/v5/tree.go +++ b/vendor/github.com/go-chi/chi/v5/tree.go @@ -730,11 +730,9 @@ func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { tail = pattern[pe] } - var rexpat string - if idx := strings.Index(key, ":"); idx >= 0 { + key, rexpat, isRegexp := strings.Cut(key, ":") + if isRegexp { nt = ntRegexp - rexpat = key[idx+1:] - key = key[:idx] } if len(rexpat) > 0 { diff --git a/vendor/modules.txt b/vendor/modules.txt index d8fcbf41..32c80864 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -87,8 +87,8 @@ github.com/getsentry/sentry-go/internal/otel/baggage/internal/baggage github.com/getsentry/sentry-go/internal/ratelimit # github.com/gin-gonic/gin v1.9.1 ## explicit; go 1.20 -# github.com/go-chi/chi/v5 v5.0.10 -## explicit; go 1.14 +# github.com/go-chi/chi/v5 v5.2.2 +## explicit; go 1.20 github.com/go-chi/chi/v5 # github.com/go-chi/cors v1.2.1 ## explicit; go 1.14 From 08efe4c103073112f6c191af027eada4949a7c0e Mon Sep 17 00:00:00 2001 From: GoncaloGarcia Date: Fri, 8 Aug 2025 15:07:54 +0100 Subject: [PATCH 2/7] Release 2025.8.0 --- RELEASE_NOTES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 6454069d..332fd13a 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,3 +1,8 @@ +2025.8.0 +- 2025-08-07 vuln: Fix GO-2025-3770 vulnerability +- 2025-07-23 TUN-9583: set proper url and hostname for cloudflared tail command +- 2025-07-07 TUN-9542: Remove unsupported Debian-based releases + 2025.7.0 - 2025-07-03 TUN-9540: Use numeric user id for Dockerfiles - 2025-07-01 TUN-9161: Remove P256Kyber768Draft00PQKex curve from nonFips curve preferences From 50104548cfaae7cdbe95c6090a9aea431016d7d9 Mon Sep 17 00:00:00 2001 From: Kevin Marshall Date: Tue, 12 Aug 2025 20:41:12 +0000 Subject: [PATCH 3/7] AUTH-7260: Add support for login interstitial auto closure Adds a switch `--auto-close` which automatically closes Access login interstitial windows/tabs immediately after the user chooses Approve or Deny. --- carrier/carrier.go | 14 +++++++------- cmd/cloudflared/access/cmd.go | 10 +++++++--- cmd/cloudflared/flags/flags.go | 3 +++ cmd/cloudflared/tunnel/login.go | 2 ++ token/token.go | 17 +++++++++-------- token/transfer.go | 12 ++++++++---- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/carrier/carrier.go b/carrier/carrier.go index b44e1324..ed493953 100644 --- a/carrier/carrier.go +++ b/carrier/carrier.go @@ -26,11 +26,12 @@ const ( ) type StartOptions struct { - AppInfo *token.AppInfo - OriginURL string - Headers http.Header - Host string - TLSClientConfig *tls.Config + AppInfo *token.AppInfo + OriginURL string + Headers http.Header + Host string + TLSClientConfig *tls.Config + AutoCloseInterstitial bool } // Connection wraps up all the needed functions to forward over the tunnel @@ -46,7 +47,6 @@ type StdinoutStream struct{} // Read will read from Stdin func (c *StdinoutStream) Read(p []byte) (int, error) { return os.Stdin.Read(p) - } // Write will write to Stdout @@ -139,7 +139,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque return nil, err } - token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, log) + token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, log) if err != nil { return nil, err } diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index cb11aede..d491cf55 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -104,6 +104,10 @@ func Commands() []*cli.Command { Name: "no-verbose", Usage: "print only the jwt to stdout", }, + &cli.BoolFlag{ + Name: "auto-close", + Usage: "automatically close the auth interstitial after action", + }, &cli.StringFlag{ Name: appURLFlag, }, @@ -322,7 +326,7 @@ func curl(c *cli.Context) error { log.Info().Msg("You don't have an Access token set. Please run access token to fetch one.") return run("curl", cmdArgs...) } - tok, err = token.FetchToken(appURL, appInfo, log) + tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), log) if err != nil { log.Err(err).Msg("Failed to refresh token") return err @@ -442,7 +446,7 @@ func sshGen(c *cli.Context) error { if err != nil { return err } - cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, log) + cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), log) if err != nil { return err } @@ -542,7 +546,7 @@ func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, if c.IsSet(sshTokenSecretFlag) { headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag)) } - options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers} + options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial)} if valid, err := isTokenValid(options, log); err != nil { return err diff --git a/cmd/cloudflared/flags/flags.go b/cmd/cloudflared/flags/flags.go index 1f8f2855..63af1dd8 100644 --- a/cmd/cloudflared/flags/flags.go +++ b/cmd/cloudflared/flags/flags.go @@ -163,4 +163,7 @@ const ( // Management hostname to signify incoming management requests ManagementHostname = "management-hostname" + + // Automatically close the login interstitial browser window after the user makes a decision. + AutoCloseInterstitial = "auto-close" ) diff --git a/cmd/cloudflared/tunnel/login.go b/cmd/cloudflared/tunnel/login.go index a5cf7813..d231e666 100644 --- a/cmd/cloudflared/tunnel/login.go +++ b/cmd/cloudflared/tunnel/login.go @@ -12,6 +12,7 @@ import ( "github.com/urfave/cli/v2" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" + cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/config" "github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/logger" @@ -97,6 +98,7 @@ func login(c *cli.Context) error { callbackStoreURL, false, false, + c.Bool(cfdflags.AutoCloseInterstitial), log, ) if err != nil { diff --git a/token/token.go b/token/token.go index 30ab9366..3966fe20 100644 --- a/token/token.go +++ b/token/token.go @@ -185,18 +185,18 @@ func Init(version string) { // FetchTokenWithRedirect will either load a stored token or generate a new one // it appends the full url as the redirect URL to the access cli request if opening the browser -func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) { - return getToken(appURL, appInfo, false, log) +func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, log *zerolog.Logger) (string, error) { + return getToken(appURL, appInfo, false, autoClose, log) } // FetchToken will either load a stored token or generate a new one // it appends the host of the appURL as the redirect URL to the access cli request if opening the browser -func FetchToken(appURL *url.URL, appInfo *AppInfo, log *zerolog.Logger) (string, error) { - return getToken(appURL, appInfo, true, log) +func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, log *zerolog.Logger) (string, error) { + return getToken(appURL, appInfo, true, autoClose, log) } // getToken will either load a stored token or generate a new one -func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog.Logger) (string, error) { +func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, log *zerolog.Logger) (string, error) { if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil { return token, nil } @@ -249,18 +249,19 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, log *zerolog. return appToken, nil } } - return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, log) + return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, autoClose, log) } // getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk, // and return the app token. -func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, log *zerolog.Logger) (string, error) { +func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, autoClose bool, log *zerolog.Logger) (string, error) { + fmt.Println("Get tokens from edge ", autoClose) // If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow. // this weird parameter is the resource name (token) and the key/value // we want to send to the transfer service. the key is token and the value // is blank (basically just the id generated in the transfer service) - resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, log) + resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, log) if err != nil { return "", errors.Wrap(err, "failed to run transfer service") } diff --git a/token/transfer.go b/token/transfer.go index fd5d80ed..d5d0e1df 100644 --- a/token/transfer.go +++ b/token/transfer.go @@ -25,12 +25,12 @@ const ( // The "dance" we refer to is building a HTTP request, opening that in a browser waiting for // the user to complete an action, while it long polls in the background waiting for an // action to be completed to download the resource. -func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, log *zerolog.Logger) ([]byte, error) { +func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, log *zerolog.Logger) ([]byte, error) { encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem") if err != nil { return nil, err } - requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly) + requestURL, err := buildRequestURL(transferURL, appAUD, key, value+encrypterClient.PublicKey(), shouldEncrypt, useHostOnly, autoClose) if err != nil { return nil, err } @@ -75,7 +75,7 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, // BuildRequestURL creates a request suitable for a resource transfer. // it will return a constructed url based off the base url and query key/value provided. // cli will build a url for cli transfer request. -func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool) (string, error) { +func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, useHostOnly bool, autoClose bool) (string, error) { q := baseURL.Query() q.Set(key, value) q.Set("aud", appAUD) @@ -90,7 +90,11 @@ func buildRequestURL(baseURL *url.URL, appAUD string, key, value string, cli, us q.Set("redirect_url", baseURL.String()) // we add the token as a query param on both the redirect_url and the main url q.Set("send_org_token", "true") // indicates that the cli endpoint should return both the org and app token q.Set("edge_token_transfer", "true") // use new LoginHelper service built on workers - baseURL.RawQuery = q.Encode() // and this actual baseURL. + if autoClose { + q.Set("close_interstitial", "true") // Automatically close the success window. + } + + baseURL.RawQuery = q.Encode() // and this actual baseURL. baseURL.Path = "cdn-cgi/access/cli" return baseURL.String(), nil } From 8825ceecb533ddb84de04549b0640c76c6c52514 Mon Sep 17 00:00:00 2001 From: Kyle Hiller Date: Tue, 19 Aug 2025 18:54:31 +0000 Subject: [PATCH 4/7] AUTH-7480 update fed callback url for login helper * AUTH-7480 update fed callback url for login helper --- carrier/carrier.go | 3 ++- cmd/cloudflared/access/carrier.go | 2 ++ cmd/cloudflared/access/cmd.go | 11 ++++++++--- cmd/cloudflared/tunnel/login.go | 10 +++++----- config/model.go | 29 +++++++++++++++-------------- token/token.go | 16 ++++++++-------- token/transfer.go | 20 ++++++++++++++++---- 7 files changed, 56 insertions(+), 35 deletions(-) diff --git a/carrier/carrier.go b/carrier/carrier.go index ed493953..5f4fc834 100644 --- a/carrier/carrier.go +++ b/carrier/carrier.go @@ -32,6 +32,7 @@ type StartOptions struct { Host string TLSClientConfig *tls.Config AutoCloseInterstitial bool + IsFedramp bool } // Connection wraps up all the needed functions to forward over the tunnel @@ -139,7 +140,7 @@ func BuildAccessRequest(options *StartOptions, log *zerolog.Logger) (*http.Reque return nil, err } - token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, log) + token, err := token.FetchTokenWithRedirect(req.URL, options.AppInfo, options.AutoCloseInterstitial, options.IsFedramp, log) if err != nil { return nil, err } diff --git a/cmd/cloudflared/access/carrier.go b/cmd/cloudflared/access/carrier.go index 11190de7..52aacc56 100644 --- a/cmd/cloudflared/access/carrier.go +++ b/cmd/cloudflared/access/carrier.go @@ -47,6 +47,7 @@ func StartForwarder(forwarder config.Forwarder, shutdown <-chan struct{}, log *z options := &carrier.StartOptions{ OriginURL: forwarder.URL, Headers: headers, //TODO: TUN-2688 support custom headers from config file + IsFedramp: forwarder.IsFedramp, } // we could add a cmd line variable for this bool if we want the SOCK5 server to be on the client side @@ -92,6 +93,7 @@ func ssh(c *cli.Context) error { OriginURL: url.String(), Headers: headers, Host: url.Host, + IsFedramp: c.Bool(fedrampFlag), } if connectTo := c.String(sshConnectTo); connectTo != "" { diff --git a/cmd/cloudflared/access/cmd.go b/cmd/cloudflared/access/cmd.go index d491cf55..636b9288 100644 --- a/cmd/cloudflared/access/cmd.go +++ b/cmd/cloudflared/access/cmd.go @@ -51,6 +51,7 @@ Host {{.Hostname}} ProxyCommand {{.Cloudflared}} access ssh --hostname %h {{end}} ` + fedrampFlag = "fedramp" ) const sentryDSN = "https://56a9c9fa5c364ab28f34b14f35ea0f1b@sentry.io/189878" @@ -79,6 +80,10 @@ func Commands() []*cli.Command { Aliases: []string{"forward"}, Category: "Access", Usage: "access ", + Flags: []cli.Flag{&cli.BoolFlag{ + Name: fedrampFlag, + Usage: "use when performing operations in fedramp account", + }}, Description: `Cloudflare Access protects internal resources by securing, authenticating and monitoring access per-user and by application. With Cloudflare Access, only authenticated users with the required permissions are able to reach sensitive resources. The commands provided here allow you to interact with Access protected @@ -326,7 +331,7 @@ func curl(c *cli.Context) error { log.Info().Msg("You don't have an Access token set. Please run access token to fetch one.") return run("curl", cmdArgs...) } - tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), log) + tok, err = token.FetchToken(appURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log) if err != nil { log.Err(err).Msg("Failed to refresh token") return err @@ -446,7 +451,7 @@ func sshGen(c *cli.Context) error { if err != nil { return err } - cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), log) + cfdToken, err := token.FetchTokenWithRedirect(fetchTokenURL, appInfo, c.Bool(cfdflags.AutoCloseInterstitial), c.Bool(fedrampFlag), log) if err != nil { return err } @@ -546,7 +551,7 @@ func verifyTokenAtEdge(appUrl *url.URL, appInfo *token.AppInfo, c *cli.Context, if c.IsSet(sshTokenSecretFlag) { headers.Add(cfAccessClientSecretHeader, c.String(sshTokenSecretFlag)) } - options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial)} + options := &carrier.StartOptions{AppInfo: appInfo, OriginURL: appUrl.String(), Headers: headers, AutoCloseInterstitial: c.Bool(cfdflags.AutoCloseInterstitial), IsFedramp: c.Bool(fedrampFlag)} if valid, err := isTokenValid(options, log); err != nil { return err diff --git a/cmd/cloudflared/tunnel/login.go b/cmd/cloudflared/tunnel/login.go index d231e666..067a8c5a 100644 --- a/cmd/cloudflared/tunnel/login.go +++ b/cmd/cloudflared/tunnel/login.go @@ -20,11 +20,10 @@ import ( ) const ( - baseLoginURL = "https://dash.cloudflare.com/argotunnel" - callbackURL = "https://login.cloudflareaccess.org/" - // For now these are the same but will change in the future once we know which URLs to use (TUN-8872) - fedBaseLoginURL = "https://dash.cloudflare.com/argotunnel" - fedCallbackStoreURL = "https://login.cloudflareaccess.org/" + baseLoginURL = "https://dash.cloudflare.com/argotunnel" + callbackURL = "https://login.cloudflareaccess.org/" + fedBaseLoginURL = "https://dash.fed.cloudflare.com/argotunnel" + fedCallbackStoreURL = "https://login.fed.cloudflareaccess.org/" fedRAMPParamName = "fedramp" loginURLParamName = "loginURL" callbackURLParamName = "callbackURL" @@ -99,6 +98,7 @@ func login(c *cli.Context) error { false, false, c.Bool(cfdflags.AutoCloseInterstitial), + isFEDRamp, log, ) if err != nil { diff --git a/config/model.go b/config/model.go index f90fd1ec..3f503a55 100644 --- a/config/model.go +++ b/config/model.go @@ -1,7 +1,7 @@ package config import ( - "crypto/md5" + "crypto/sha256" "fmt" "io" "strings" @@ -16,6 +16,7 @@ type Forwarder struct { TokenClientID string `json:"service_token_id" yaml:"serviceTokenID"` TokenSecret string `json:"secret_token_id" yaml:"serviceTokenSecret"` Destination string `json:"destination"` + IsFedramp bool `json:"is_fedramp" yaml:"isFedramp"` } // Tunnel represents a tunnel that should be started @@ -46,24 +47,24 @@ type Root struct { // Hash returns the computed values to see if the forwarder values change func (f *Forwarder) Hash() string { - h := md5.New() - io.WriteString(h, f.URL) - io.WriteString(h, f.Listener) - io.WriteString(h, f.TokenClientID) - io.WriteString(h, f.TokenSecret) - io.WriteString(h, f.Destination) + h := sha256.New() + _, _ = io.WriteString(h, f.URL) + _, _ = io.WriteString(h, f.Listener) + _, _ = io.WriteString(h, f.TokenClientID) + _, _ = io.WriteString(h, f.TokenSecret) + _, _ = io.WriteString(h, f.Destination) return fmt.Sprintf("%x", h.Sum(nil)) } // Hash returns the computed values to see if the forwarder values change func (r *DNSResolver) Hash() string { - h := md5.New() - io.WriteString(h, r.Address) - io.WriteString(h, strings.Join(r.Bootstraps, ",")) - io.WriteString(h, strings.Join(r.Upstreams, ",")) - io.WriteString(h, fmt.Sprintf("%d", r.Port)) - io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections)) - io.WriteString(h, fmt.Sprintf("%v", r.Enabled)) + h := sha256.New() + _, _ = io.WriteString(h, r.Address) + _, _ = io.WriteString(h, strings.Join(r.Bootstraps, ",")) + _, _ = io.WriteString(h, strings.Join(r.Upstreams, ",")) + _, _ = io.WriteString(h, fmt.Sprintf("%d", r.Port)) + _, _ = io.WriteString(h, fmt.Sprintf("%d", r.MaxUpstreamConnections)) + _, _ = io.WriteString(h, fmt.Sprintf("%v", r.Enabled)) return fmt.Sprintf("%x", h.Sum(nil)) } diff --git a/token/token.go b/token/token.go index 3966fe20..555a7f69 100644 --- a/token/token.go +++ b/token/token.go @@ -185,18 +185,18 @@ func Init(version string) { // FetchTokenWithRedirect will either load a stored token or generate a new one // it appends the full url as the redirect URL to the access cli request if opening the browser -func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, log *zerolog.Logger) (string, error) { - return getToken(appURL, appInfo, false, autoClose, log) +func FetchTokenWithRedirect(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { + return getToken(appURL, appInfo, false, autoClose, isFedramp, log) } // FetchToken will either load a stored token or generate a new one // it appends the host of the appURL as the redirect URL to the access cli request if opening the browser -func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, log *zerolog.Logger) (string, error) { - return getToken(appURL, appInfo, true, autoClose, log) +func FetchToken(appURL *url.URL, appInfo *AppInfo, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { + return getToken(appURL, appInfo, true, autoClose, isFedramp, log) } // getToken will either load a stored token or generate a new one -func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, log *zerolog.Logger) (string, error) { +func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { if token, err := GetAppTokenIfExists(appInfo); token != "" && err == nil { return token, nil } @@ -249,19 +249,19 @@ func getToken(appURL *url.URL, appInfo *AppInfo, useHostOnly bool, autoClose boo return appToken, nil } } - return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, autoClose, log) + return getTokensFromEdge(appURL, appInfo.AppAUD, appTokenPath, orgTokenPath, useHostOnly, autoClose, isFedramp, log) } // getTokensFromEdge will attempt to use the transfer service to retrieve an app and org token, save them to disk, // and return the app token. -func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, autoClose bool, log *zerolog.Logger) (string, error) { +func getTokensFromEdge(appURL *url.URL, appAUD, appTokenPath, orgTokenPath string, useHostOnly bool, autoClose bool, isFedramp bool, log *zerolog.Logger) (string, error) { fmt.Println("Get tokens from edge ", autoClose) // If no org token exists or if it couldn't be exchanged for an app token, then run the transfer service flow. // this weird parameter is the resource name (token) and the key/value // we want to send to the transfer service. the key is token and the value // is blank (basically just the id generated in the transfer service) - resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, log) + resourceData, err := RunTransfer(appURL, appAUD, keyName, keyName, "", true, useHostOnly, autoClose, isFedramp, log) if err != nil { return "", errors.Wrap(err, "failed to run transfer service") } diff --git a/token/transfer.go b/token/transfer.go index d5d0e1df..85cbb9a5 100644 --- a/token/transfer.go +++ b/token/transfer.go @@ -16,6 +16,7 @@ import ( const ( baseStoreURL = "https://login.cloudflareaccess.org/" + fedStoreURL = "https://login.fed.cloudflareaccess.org/" clientTimeout = time.Second * 60 ) @@ -25,7 +26,7 @@ const ( // The "dance" we refer to is building a HTTP request, opening that in a browser waiting for // the user to complete an action, while it long polls in the background waiting for an // action to be completed to download the resource. -func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, log *zerolog.Logger) ([]byte, error) { +func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, shouldEncrypt bool, useHostOnly bool, autoClose bool, fedramp bool, log *zerolog.Logger) ([]byte, error) { encrypterClient, err := NewEncrypter("cloudflared_priv.pem", "cloudflared_pub.pem") if err != nil { return nil, err @@ -45,8 +46,14 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, var resourceData []byte + storeURL := baseStoreURL + + if fedramp { + storeURL = fedStoreURL + } + if shouldEncrypt { - buf, key, err := transferRequest(baseStoreURL+"transfer/"+encrypterClient.PublicKey(), log) + buf, key, err := transferRequest(storeURL+"transfer/"+encrypterClient.PublicKey(), log) if err != nil { return nil, err } @@ -62,7 +69,7 @@ func RunTransfer(transferURL *url.URL, appAUD, resourceName, key, value string, resourceData = decrypted } else { - buf, _, err := transferRequest(baseStoreURL+encrypterClient.PublicKey(), log) + buf, _, err := transferRequest(storeURL+encrypterClient.PublicKey(), log) if err != nil { return nil, err } @@ -131,7 +138,12 @@ func poll(client *http.Client, requestURL string, log *zerolog.Logger) ([]byte, // ignore everything other than server errors as the resource // may not exist until the user does the interaction if resp.StatusCode >= 500 { - return nil, "", fmt.Errorf("error on request %d", resp.StatusCode) + buf := new(bytes.Buffer) + if _, err := io.Copy(buf, resp.Body); err != nil { + return nil, "", err + } + + return nil, "", fmt.Errorf("error on request %d: %s", resp.StatusCode, buf.String()) } if resp.StatusCode != 200 { log.Info().Msg("Waiting for login...") From 41dffd7f3cbe768bdadcbb55eb216b99a5d93914 Mon Sep 17 00:00:00 2001 From: Devin Carr Date: Tue, 19 Aug 2025 16:10:00 -0700 Subject: [PATCH 5/7] CUSTESC-53681: Correct QUIC connection management for datagram handlers Corrects the pattern of using errgroup's and context cancellation to simplify the logic for canceling extra routines for the QUIC connection. This is because the extra context cancellation is redundant with the fact that the errgroup also cancels it's own provided context when a routine returns (error or not). For the datagram handler specifically, since it can respond faster to a context cancellation from the QUIC connection, we wrap the error before surfacing it outside of the QUIC connection scope to the supervisor. Additionally, the supervisor will look for this error type to check if it should retry the QUIC connection. These two operations are required because the supervisor does not look for a context canceled error when deciding to retry a connection. If a context canceled from the datagram handler were to be returned up to the supervisor on the initial connection, the cloudflared application would exit. We want to ensure that cloudflared maintains connection attempts even if any of the services on-top of a QUIC connection fail (datagram handler in this case). Additional logging is also introduced along these paths to help with understanding the error conditions from the specific handlers on-top of a QUIC connection. Related CUSTESC-53681 Closes TUN-9610 --- connection/control.go | 2 +- connection/errors.go | 35 +++++++++++------------ connection/observer.go | 9 ++++++ connection/quic_connection.go | 46 ++++++++++++++++++------------ connection/quic_connection_test.go | 3 +- connection/quic_datagram_v2.go | 11 ++----- quic/v3/muxer.go | 2 +- supervisor/supervisor.go | 16 +++++------ supervisor/tunnel.go | 9 ++---- 9 files changed, 70 insertions(+), 63 deletions(-) diff --git a/connection/control.go b/connection/control.go index d590b078..23ddedb1 100644 --- a/connection/control.go +++ b/connection/control.go @@ -82,7 +82,7 @@ func (c *controlStream) ServeControlStream( tunnelConfigGetter TunnelConfigJSONGetter, ) error { registrationClient := c.registerClientFunc(ctx, rw, c.registerTimeout) - + c.observer.logConnecting(c.connIndex, c.edgeAddress, c.protocol) registrationDetails, err := registrationClient.RegisterConnection( ctx, c.tunnelProperties.Credentials.Auth(), diff --git a/connection/errors.go b/connection/errors.go index 1bb34d6d..2cc60566 100644 --- a/connection/errors.go +++ b/connection/errors.go @@ -1,7 +1,6 @@ package connection import ( - "github.com/cloudflare/cloudflared/edgediscovery" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) @@ -53,26 +52,26 @@ func serverRegistrationErrorFromRPC(err error) ServerRegisterTunnelError { } } -type muxerShutdownError struct{} +type ControlStreamError struct{} -func (e muxerShutdownError) Error() string { - return "muxer shutdown" +var _ error = &ControlStreamError{} + +func (e *ControlStreamError) Error() string { + return "control stream encountered a failure while serving" } -var errMuxerStopped = muxerShutdownError{} +type StreamListenerError struct{} -func isHandshakeErrRecoverable(err error, connIndex uint8, observer *Observer) bool { - log := observer.log.With(). - Uint8(LogFieldConnIndex, connIndex). - Err(err). - Logger() +var _ error = &StreamListenerError{} - switch err.(type) { - case edgediscovery.DialError: - log.Error().Msg("Connection unable to dial edge") - default: - log.Error().Msg("Connection failed") - return false - } - return true +func (e *StreamListenerError) Error() string { + return "accept stream listener encountered a failure while serving" +} + +type DatagramManagerError struct{} + +var _ error = &DatagramManagerError{} + +func (e *DatagramManagerError) Error() string { + return "datagram manager encountered a failure while serving" } diff --git a/connection/observer.go b/connection/observer.go index 817e6d2e..7e63cb3d 100644 --- a/connection/observer.go +++ b/connection/observer.go @@ -46,6 +46,15 @@ func (o *Observer) RegisterSink(sink EventSink) { o.addSinkChan <- sink } +func (o *Observer) logConnecting(connIndex uint8, address net.IP, protocol Protocol) { + o.log.Debug(). + Int(management.EventTypeKey, int(management.Cloudflared)). + Uint8(LogFieldConnIndex, connIndex). + IPAddr(LogFieldIPAddress, address). + Str(LogFieldProtocol, protocol.String()). + Msg("Registering tunnel connection") +} + func (o *Observer) logConnected(connectionID uuid.UUID, connIndex uint8, location string, address net.IP, protocol Protocol) { o.log.Info(). Int(management.EventTypeKey, int(management.Cloudflared)). diff --git a/connection/quic_connection.go b/connection/quic_connection.go index 74e1f4a4..1ca5647e 100644 --- a/connection/quic_connection.go +++ b/connection/quic_connection.go @@ -3,6 +3,7 @@ package connection import ( "bufio" "context" + "errors" "fmt" "io" "net" @@ -12,7 +13,6 @@ import ( "sync/atomic" "time" - "github.com/pkg/errors" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" @@ -65,7 +65,7 @@ func NewTunnelConnection( streamWriteTimeout time.Duration, gracePeriod time.Duration, logger *zerolog.Logger, -) (TunnelConnection, error) { +) TunnelConnection { return &quicConnection{ conn: conn, logger: logger, @@ -77,10 +77,11 @@ func NewTunnelConnection( rpcTimeout: rpcTimeout, streamWriteTimeout: streamWriteTimeout, gracePeriod: gracePeriod, - }, nil + } } // Serve starts a QUIC connection that begins accepting streams. +// Returning a nil error means cloudflared will exit for good and will not attempt to reconnect. func (q *quicConnection) Serve(ctx context.Context) error { // The edge assumes the first stream is used for the control plane controlStream, err := q.conn.OpenStream() @@ -88,16 +89,16 @@ func (q *quicConnection) Serve(ctx context.Context) error { return fmt.Errorf("failed to open a registration control stream: %w", err) } - // If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits - // as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this - // connection). // If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the - // other goroutine as fast as possible. - ctx, cancel := context.WithCancel(ctx) + // other goroutines. We enforce returning a not-nil error for each function started in the errgroup by logging + // the error returned and returning a custom error type instead. errGroup, ctx := errgroup.WithContext(ctx) - // In the future, if cloudflared can autonomously push traffic to the edge, we have to make sure the control - // stream is already fully registered before the other goroutines can proceed. + // Close the quic connection if any of the following routines return from the errgroup (regardless of their error) + // because they are no longer processing requests for the connection. + defer q.Close() + + // Start the control stream routine errGroup.Go(func() error { // err is equal to nil if we exit due to unregistration. If that happens we want to wait the full // amount of the grace period, allowing requests to finish before we cancel the context, which will @@ -114,16 +115,26 @@ func (q *quicConnection) Serve(ctx context.Context) error { } } } - cancel() - return err + if err != nil { + q.logger.Error().Err(err).Msg("failed to serve the control stream") + } + return &ControlStreamError{} }) + // Start the accept stream loop routine errGroup.Go(func() error { - defer cancel() - return q.acceptStream(ctx) + err := q.acceptStream(ctx) + if err != nil { + q.logger.Error().Err(err).Msg("failed to accept incoming stream requests") + } + return &StreamListenerError{} }) + // Start the datagram handler routine errGroup.Go(func() error { - defer cancel() - return q.datagramHandler.Serve(ctx) + err := q.datagramHandler.Serve(ctx) + if err != nil { + q.logger.Error().Err(err).Msg("failed to run the datagram handler") + } + return &DatagramManagerError{} }) return errGroup.Wait() @@ -140,7 +151,6 @@ func (q *quicConnection) Close() { } func (q *quicConnection) acceptStream(ctx context.Context) error { - defer q.Close() for { quicStream, err := q.conn.AcceptStream(ctx) if err != nil { @@ -230,7 +240,7 @@ func (q *quicConnection) dispatchRequest(ctx context.Context, stream *rpcquic.Re ConnIndex: q.connIndex, }), rwa.connectResponseSent default: - return errors.Errorf("unsupported error type: %s", request.Type), false + return fmt.Errorf("unsupported error type: %s", request.Type), false } } diff --git a/connection/quic_connection_test.go b/connection/quic_connection_test.go index 8765fd29..fc831532 100644 --- a/connection/quic_connection_test.go +++ b/connection/quic_connection_test.go @@ -847,7 +847,7 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8) &log, } - tunnelConn, err := NewTunnelConnection( + tunnelConn := NewTunnelConnection( ctx, conn, index, @@ -860,7 +860,6 @@ func testTunnelConnection(t *testing.T, serverAddr netip.AddrPort, index uint8) 0*time.Second, &log, ) - require.NoError(t, err) return tunnelConn, datagramConn } diff --git a/connection/quic_datagram_v2.go b/connection/quic_datagram_v2.go index aebead70..e39f06e6 100644 --- a/connection/quic_datagram_v2.go +++ b/connection/quic_datagram_v2.go @@ -98,24 +98,17 @@ func NewDatagramV2Connection(ctx context.Context, } func (d *datagramV2Connection) Serve(ctx context.Context) error { - // If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits - // as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this - // connection). - // If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the - // other goroutine as fast as possible. - ctx, cancel := context.WithCancel(ctx) + // If either goroutine from the errgroup returns at all (error or nil), we rely on its cancellation to make sure + // the other goroutines as well. errGroup, ctx := errgroup.WithContext(ctx) errGroup.Go(func() error { - defer cancel() return d.sessionManager.Serve(ctx) }) errGroup.Go(func() error { - defer cancel() return d.datagramMuxer.ServeReceive(ctx) }) errGroup.Go(func() error { - defer cancel() return d.packetRouter.Serve(ctx) }) diff --git a/quic/v3/muxer.go b/quic/v3/muxer.go index 4f5605bf..6a614814 100644 --- a/quic/v3/muxer.go +++ b/quic/v3/muxer.go @@ -175,7 +175,7 @@ func (c *datagramConn) Serve(ctx context.Context) error { // Monitor the context of cloudflared case <-ctx.Done(): return ctx.Err() - // Monitor the context of the underlying connection + // Monitor the context of the underlying quic connection case <-connCtx.Done(): return connCtx.Err() // Monitor for any hard errors from reading the connection diff --git a/supervisor/supervisor.go b/supervisor/supervisor.go index cb25d68a..5bd49749 100644 --- a/supervisor/supervisor.go +++ b/supervisor/supervisor.go @@ -132,6 +132,7 @@ func (s *Supervisor) Run( if err == errEarlyShutdown { return nil } + s.log.Logger().Error().Err(err).Msg("initial tunnel connection failed") return err } var tunnelsWaiting []int @@ -154,6 +155,7 @@ func (s *Supervisor) Run( // (note that this may also be caused by context cancellation) case tunnelError := <-s.tunnelErrors: tunnelsActive-- + s.log.ConnAwareLogger().Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated") if tunnelError.err != nil && !shuttingDown { switch tunnelError.err.(type) { case ReconnectSignal: @@ -166,7 +168,6 @@ func (s *Supervisor) Run( if _, retry := s.tunnelsProtocolFallback[tunnelError.index].GetMaxBackoffDuration(ctx); !retry { continue } - s.log.ConnAwareLogger().Err(tunnelError.err).Int(connection.LogFieldConnIndex, tunnelError.index).Msg("Connection terminated") tunnelsWaiting = append(tunnelsWaiting, tunnelError.index) s.waitForNextTunnel(tunnelError.index) @@ -285,7 +286,10 @@ func (s *Supervisor) startFirstTunnel( *quic.IdleTimeoutError, *quic.ApplicationError, edgediscovery.DialError, - *connection.EdgeQuicDialError: + *connection.EdgeQuicDialError, + *connection.ControlStreamError, + *connection.StreamListenerError, + *connection.DatagramManagerError: // Try again for these types of errors default: // Uncaught errors should bail startup @@ -301,13 +305,9 @@ func (s *Supervisor) startTunnel( index int, connectedSignal *signal.Signal, ) { - var err error - defer func() { - s.tunnelErrors <- tunnelError{index: index, err: err} - }() - // nolint: gosec - err = s.edgeTunnelServer.Serve(ctx, uint8(index), s.tunnelsProtocolFallback[index], connectedSignal) + err := s.edgeTunnelServer.Serve(ctx, uint8(index), s.tunnelsProtocolFallback[index], connectedSignal) + s.tunnelErrors <- tunnelError{index: index, err: err} } func (s *Supervisor) newConnectedTunnelSignal(index int) *signal.Signal { diff --git a/supervisor/tunnel.go b/supervisor/tunnel.go index b73eecb9..97013f5a 100644 --- a/supervisor/tunnel.go +++ b/supervisor/tunnel.go @@ -556,6 +556,7 @@ func (e *EdgeTunnelServer) serveQUIC( pqMode := connOptions.FeatureSnapshot.PostQuantum curvePref, err := curvePreference(pqMode, fips.IsFipsEnabled(), tlsConfig.CurvePreferences) if err != nil { + connLogger.ConnAwareLogger().Err(err).Msgf("failed to get curve preferences") return err, true } @@ -627,7 +628,7 @@ func (e *EdgeTunnelServer) serveQUIC( } // Wrap the [quic.Connection] as a TunnelConnection - tunnelConn, err := connection.NewTunnelConnection( + tunnelConn := connection.NewTunnelConnection( ctx, conn, connIndex, @@ -640,17 +641,13 @@ func (e *EdgeTunnelServer) serveQUIC( e.config.GracePeriod, connLogger.Logger(), ) - if err != nil { - connLogger.ConnAwareLogger().Err(err).Msgf("Failed to create new tunnel connection") - return err, true - } // Serve the TunnelConnection errGroup, serveCtx := errgroup.WithContext(ctx) errGroup.Go(func() error { err := tunnelConn.Serve(serveCtx) if err != nil { - connLogger.ConnAwareLogger().Err(err).Msg("Failed to serve tunnel connection") + connLogger.ConnAwareLogger().Err(err).Msg("failed to serve tunnel connection") } return err }) From f9c2bd51ae2439b099b88c5c52229758f8e03f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20=22Pisco=22=20Fernandes?= Date: Thu, 21 Aug 2025 10:53:31 +0100 Subject: [PATCH 6/7] Release 2025.8.1 --- RELEASE_NOTES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 332fd13a..03c32dd8 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,3 +1,8 @@ +2025.8.1 +- 2025-08-19 AUTH-7480 update fed callback url for login helper +- 2025-08-19 CUSTESC-53681: Correct QUIC connection management for datagram handlers +- 2025-08-12 AUTH-7260: Add support for login interstitial auto closure + 2025.8.0 - 2025-08-07 vuln: Fix GO-2025-3770 vulnerability - 2025-07-23 TUN-9583: set proper url and hostname for cloudflared tail command From 9e6d58aaea0fcad6b4ae6ac958890af17f3a66a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Garcia?= Date: Wed, 27 Aug 2025 15:57:58 +0000 Subject: [PATCH 7/7] TUN-9755: Set endpoint in tunnel credentials when generating locally managed tunnel with a Fed token * TUN-9755: Set endpoint in tunnel credentials when generating locally managed tunnel with a Fed token Closes TUN-9755 --- cmd/cloudflared/tunnel/subcommand_context.go | 2 ++ credentials/credentials.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/cmd/cloudflared/tunnel/subcommand_context.go b/cmd/cloudflared/tunnel/subcommand_context.go index 205fe51c..c1eb8808 100644 --- a/cmd/cloudflared/tunnel/subcommand_context.go +++ b/cmd/cloudflared/tunnel/subcommand_context.go @@ -155,10 +155,12 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec if err != nil { return nil, err } + tunnelCredentials := connection.Credentials{ AccountTag: credential.AccountID(), TunnelSecret: tunnelSecret, TunnelID: tunnel.ID, + Endpoint: credential.Endpoint(), } usedCertPath := false if credentialsFilePath == "" { diff --git a/credentials/credentials.go b/credentials/credentials.go index 7abd9ae4..eb63cf22 100644 --- a/credentials/credentials.go +++ b/credentials/credentials.go @@ -23,6 +23,10 @@ func (c User) AccountID() string { return c.cert.AccountID } +func (c User) Endpoint() string { + return c.cert.Endpoint +} + func (c User) ZoneID() string { return c.cert.ZoneID }