chore(deps): bump Go to 1.26 and migrate upstream reverse proxies to Rewrite

Signed-off-by: Jan Larwig <jan@larwig.com>
This commit is contained in:
Jan Larwig
2026-06-08 14:12:56 +02:00
parent 9a14186a26
commit 0de18825f6
9 changed files with 211 additions and 54 deletions
+57 -18
View File
@@ -123,7 +123,7 @@ func (t *unixRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
// The proxy should render an error page if there are failures connecting to the
// upstream server.
func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler ProxyErrorHandler) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(target)
proxy := newSingleHostReverseProxy(target)
// Inherit default transport options from Go's stdlib
transport := http.DefaultTransport.(*http.Transport).Clone()
@@ -155,7 +155,7 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
}
// Ensure we always pass the original request path
setProxyDirector(proxy)
setProxyRewrite(proxy)
// TODO (@tuunit) - this should be inverted or get a better name in the future to set the upstream host header
// only if PassHostHeader is explicitly set to true. Currently this would be a breaking change.
@@ -179,32 +179,71 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
return proxy
}
// setProxyUpstreamHostHeader sets the proxy.Director so that upstream requests
// receive a host header matching the target URL.
func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
req.Host = target.Host
func newSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy {
return &httputil.ReverseProxy{
Rewrite: func(proxyReq *httputil.ProxyRequest) {
proxyReq.SetURL(target)
proxyReq.Out.Host = proxyReq.In.Host
setProxyForwardingHeaders(proxyReq)
},
}
}
// setProxyDirector sets the proxy.Director so that request URIs are escaped
// setProxyUpstreamHostHeader sets the proxy.Rewrite so that upstream requests
// receive a host header matching the target URL.
func setProxyUpstreamHostHeader(proxy *httputil.ReverseProxy, target *url.URL) {
rewrite := proxy.Rewrite
proxy.Rewrite = func(proxyReq *httputil.ProxyRequest) {
rewrite(proxyReq)
proxyReq.Out.Host = target.Host
}
}
// setProxyRewrite sets the proxy.Rewrite so that request URIs are escaped
// when proxying to usptream servers.
func setProxyDirector(proxy *httputil.ReverseProxy) {
director := proxy.Director
proxy.Director = func(req *http.Request) {
director(req)
func setProxyRewrite(proxy *httputil.ReverseProxy) {
rewrite := proxy.Rewrite
proxy.Rewrite = func(proxyReq *httputil.ProxyRequest) {
rewrite(proxyReq)
// use RequestURI so that we aren't unescaping encoded slashes in the request path
req.URL.Opaque = req.RequestURI
req.URL.RawQuery = ""
req.URL.ForceQuery = false
proxyReq.Out.URL.Opaque = proxyReq.In.RequestURI
proxyReq.Out.URL.RawQuery = ""
proxyReq.Out.URL.ForceQuery = false
}
}
func setProxyForwardingHeaders(proxyReq *httputil.ProxyRequest) {
// TODO (@tuunit): Preserve the legacy Director-based forwarding header behavior
// for backwards compatibility. Harden this with saner defaults and/or
// explicit flags in the future.
for _, header := range []string{"Forwarded", "X-Forwarded-Host", "X-Forwarded-Proto"} {
if values, ok := proxyReq.In.Header[header]; ok {
proxyReq.Out.Header[header] = append([]string(nil), values...)
}
}
prior, ok := proxyReq.In.Header["X-Forwarded-For"]
if ok {
proxyReq.Out.Header["X-Forwarded-For"] = append([]string(nil), prior...)
}
clientIP, _, err := net.SplitHostPort(proxyReq.In.RemoteAddr)
if err != nil {
return
}
omit := ok && prior == nil
if len(prior) > 0 {
clientIP = strings.Join(prior, ", ") + ", " + clientIP
}
if !omit {
proxyReq.Out.Header.Set("X-Forwarded-For", clientIP)
}
}
// newWebSocketReverseProxy creates a new reverse proxy for proxying websocket connections.
func newWebSocketReverseProxy(u *url.URL, skipTLSVerify *bool, passHostHeader *bool) http.Handler {
wsProxy := httputil.NewSingleHostReverseProxy(u)
wsProxy := newSingleHostReverseProxy(u)
// Inherit default transport options from Go's stdlib
transport := http.DefaultTransport.(*http.Transport).Clone()
+55 -1
View File
@@ -361,7 +361,12 @@ var _ = Describe("HTTP Upstream Suite", func() {
return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
proxy, ok := h.(*httputil.ReverseProxy)
Expect(ok).To(BeTrue())
proxy.Director(req)
outReq := req.Clone(req.Context())
proxy.Rewrite(&httputil.ProxyRequest{
In: req,
Out: outReq,
})
req.Host = outReq.Host
})
}
httpUpstream.handler = requestInterceptor(httpUpstream.handler)
@@ -370,6 +375,54 @@ var _ = Describe("HTTP Upstream Suite", func() {
Expect(req.Host).To(Equal(strings.TrimPrefix(serverAddr, "http://")))
})
It("ServeHTTP preserves forwarding headers when using Rewrite", func() {
req := httptest.NewRequest("", "http://example.localhost/foo", nil)
req.RemoteAddr = "192.0.2.10:1234"
req.Header.Set("Forwarded", "for=192.0.2.1;proto=https;host=example.localhost")
req.Header.Set("X-Forwarded-Host", "forwarded.example.localhost")
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-For", "192.0.2.1")
req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{})
rw := httptest.NewRecorder()
upstream := options.Upstream{
ID: "preserveForwardedHeaders",
PassHostHeader: ptr.To(true),
ProxyWebSockets: ptr.To(false),
InsecureSkipTLSVerify: ptr.To(false),
FlushInterval: &defaultFlushInterval,
Timeout: &defaultTimeout,
}
u, err := url.Parse(serverAddr)
Expect(err).ToNot(HaveOccurred())
handler := newHTTPUpstreamProxy(upstream, u, nil, nil)
httpUpstream, ok := handler.(*httpUpstreamProxy)
Expect(ok).To(BeTrue())
requestInterceptor := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
proxy, ok := h.(*httputil.ReverseProxy)
Expect(ok).To(BeTrue())
outReq := req.Clone(req.Context())
proxy.Rewrite(&httputil.ProxyRequest{
In: req,
Out: outReq,
})
Expect(outReq.Header.Values("Forwarded")).To(Equal([]string{"for=192.0.2.1;proto=https;host=example.localhost"}))
Expect(outReq.Header.Values("X-Forwarded-Host")).To(Equal([]string{"forwarded.example.localhost"}))
Expect(outReq.Header.Values("X-Forwarded-Proto")).To(Equal([]string{"https"}))
Expect(outReq.Header.Values("X-Forwarded-For")).To(Equal([]string{"192.0.2.1, 192.0.2.10"}))
})
}
httpUpstream.handler = requestInterceptor(httpUpstream.handler)
httpUpstream.ServeHTTP(rw, req)
})
type newUpstreamTableInput struct {
proxyWebSockets bool
flushInterval time.Duration
@@ -405,6 +458,7 @@ var _ = Describe("HTTP Upstream Suite", func() {
proxy, ok := upstreamProxy.handler.(*httputil.ReverseProxy)
Expect(ok).To(BeTrue())
Expect(proxy.Rewrite).ToNot(BeNil())
Expect(proxy.FlushInterval).To(Equal(in.flushInterval))
transport, ok := proxy.Transport.(*http.Transport)
Expect(ok).To(BeTrue())
+1 -1
View File
@@ -79,7 +79,7 @@ var _ = Describe("Proxy Suite", func() {
{
ID: "bad-http-backend",
Path: "/bad-http/",
URI: "http://::1",
URI: invalidServer,
},
{
ID: "single-path-backend",
+1 -1
View File
@@ -24,7 +24,7 @@ var (
serverAddr string
unixServer *httptest.Server
unixServerAddr string
invalidServer = "http://::1"
invalidServer = "http://127.0.0.1:1"
)
func TestUpstreamSuite(t *testing.T) {
+2 -2
View File
@@ -193,8 +193,8 @@ var _ = Describe("Sessions", func() {
unreachableRedisDelMsg = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
unrechableSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: redis: nil"
refusedSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: context deadline exceeded"
refusedSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: context deadline exceeded"
refusedSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels specified in configuration are unreachable: dial tcp 127.0.0.1:65535: connect: connection refused"
refusedSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels specified in configuration are unreachable: dial tcp 127.0.0.1:65535: connect: connection refused"
)
type redisStoreTableInput struct {