caddyhttp: wrap accepted connection to suppress tls.ConnectionState (#7247)

This commit is contained in:
WeidiDeng 2025-10-16 11:13:40 +08:00 committed by GitHub
parent d7185fd002
commit 1ce2a13ad1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 166 additions and 11 deletions

View file

@ -0,0 +1,129 @@
package integration
import (
"fmt"
"net/http"
"slices"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
func newH2ListenerWithVersionsWithTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
const baseConfig = `
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
servers :9443 {
protocols %s
}
}
localhost {
respond "{http.request.tls.proto} {http.request.proto}"
}
`
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
tr := tester.Client.Transport.(*http.Transport)
tr.TLSClientConfig.NextProtos = clientVersions
tr.Protocols = new(http.Protocols)
if slices.Contains(clientVersions, "h2") {
tr.ForceAttemptHTTP2 = true
tr.Protocols.SetHTTP2(true)
}
if !slices.Contains(clientVersions, "http/1.1") {
tr.Protocols.SetHTTP1(false)
}
return tester
}
func TestH2ListenerWithTLS(t *testing.T) {
tests := []struct {
serverVersions []string
clientVersions []string
expectedBody string
failed bool
}{
{[]string{"h2"}, []string{"h2"}, "h2 HTTP/2.0", false},
{[]string{"h2"}, []string{"http/1.1"}, "", true},
{[]string{"h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
{[]string{"h1"}, []string{"h2"}, "", true},
{[]string{"h2", "h1"}, []string{"h2"}, "h2 HTTP/2.0", false},
{[]string{"h2", "h1"}, []string{"http/1.1"}, "http/1.1 HTTP/1.1", false},
}
for _, tc := range tests {
tester := newH2ListenerWithVersionsWithTLSTester(t, tc.serverVersions, tc.clientVersions)
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
if tc.failed {
resp, err := tester.Client.Get("https://localhost:9443")
if err == nil {
t.Errorf("unexpected response: %d", resp.StatusCode)
}
} else {
tester.AssertGetResponse("https://localhost:9443", 200, tc.expectedBody)
}
}
}
func newH2ListenerWithVersionsWithoutTLSTester(t *testing.T, serverVersions []string, clientVersions []string) *caddytest.Tester {
const baseConfig = `
{
skip_install_trust
admin localhost:2999
http_port 9080
servers :9080 {
protocols %s
}
}
http://localhost {
respond "{http.request.proto}"
}
`
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(baseConfig, strings.Join(serverVersions, " ")), "caddyfile")
tr := tester.Client.Transport.(*http.Transport)
tr.Protocols = new(http.Protocols)
if slices.Contains(clientVersions, "h2c") {
tr.Protocols.SetHTTP1(false)
tr.Protocols.SetUnencryptedHTTP2(true)
} else if slices.Contains(clientVersions, "http/1.1") {
tr.Protocols.SetHTTP1(true)
tr.Protocols.SetUnencryptedHTTP2(false)
}
return tester
}
func TestH2ListenerWithoutTLS(t *testing.T) {
tests := []struct {
serverVersions []string
clientVersions []string
expectedBody string
failed bool
}{
{[]string{"h2c"}, []string{"h2c"}, "HTTP/2.0", false},
{[]string{"h2c"}, []string{"http/1.1"}, "", true},
{[]string{"h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
{[]string{"h1"}, []string{"h2c"}, "", true},
{[]string{"h2c", "h1"}, []string{"h2c"}, "HTTP/2.0", false},
{[]string{"h2c", "h1"}, []string{"http/1.1"}, "HTTP/1.1", false},
}
for _, tc := range tests {
tester := newH2ListenerWithVersionsWithoutTLSTester(t, tc.serverVersions, tc.clientVersions)
t.Logf("running with server versions %v and client versions %v:", tc.serverVersions, tc.clientVersions)
if tc.failed {
resp, err := tester.Client.Get("http://localhost:9080")
if err == nil {
t.Errorf("unexpected response: %d", resp.StatusCode)
}
} else {
tester.AssertGetResponse("http://localhost:9080", 200, tc.expectedBody)
}
}
}

View file

@ -466,7 +466,14 @@ func (app *App) Start() error {
ErrorLog: serverLogger, ErrorLog: serverLogger,
Protocols: new(http.Protocols), Protocols: new(http.Protocols),
ConnContext: func(ctx context.Context, c net.Conn) context.Context { ConnContext: func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, ConnCtxKey, c) if nc, ok := c.(interface{ tlsNetConn() net.Conn }); ok {
getTlsConStateFunc := sync.OnceValue(func() *tls.ConnectionState {
tlsConnState := nc.tlsNetConn().(connectionStater).ConnectionState()
return &tlsConnState
})
ctx = context.WithValue(ctx, tlsConnectionStateFuncCtxKey, getTlsConStateFunc)
}
return ctx
}, },
} }

View file

@ -36,6 +36,12 @@ func (h *http2Listener) Accept() (net.Conn, error) {
return nil, err return nil, err
} }
// *tls.Conn doesn't need to be wrapped because we already removed unwanted alpns
// and handshake won't succeed without mutually supported alpns
if tlsConn, ok := conn.(*tls.Conn); ok {
return tlsConn, nil
}
_, isConnectionStater := conn.(connectionStater) _, isConnectionStater := conn.(connectionStater)
// emit a warning // emit a warning
if h.useTLS && !isConnectionStater { if h.useTLS && !isConnectionStater {
@ -46,6 +52,9 @@ func (h *http2Listener) Accept() (net.Conn, error) {
// if both h1 and h2 are enabled, we don't need to check the preface // if both h1 and h2 are enabled, we don't need to check the preface
if h.useH1 && h.useH2 { if h.useH1 && h.useH2 {
if isConnectionStater {
return tlsStateConn{conn}, nil
}
return conn, nil return conn, nil
} }
@ -53,14 +62,26 @@ func (h *http2Listener) Accept() (net.Conn, error) {
// or else the listener wouldn't be created // or else the listener wouldn't be created
h2Conn := &http2Conn{ h2Conn := &http2Conn{
h2Expected: h.useH2, h2Expected: h.useH2,
logger: h.logger,
Conn: conn, Conn: conn,
} }
if isConnectionStater { if isConnectionStater {
return http2StateConn{h2Conn}, nil return tlsStateConn{http2StateConn{h2Conn}}, nil
} }
return h2Conn, nil return h2Conn, nil
} }
// tlsStateConn wraps a net.Conn that implements connectionStater to hide that method
// we can call netConn to get the original net.Conn and get the tls connection state
// golang 1.25 will call that method, and it breaks h2 with connections other than *tls.Conn
type tlsStateConn struct {
net.Conn
}
func (conn tlsStateConn) tlsNetConn() net.Conn {
return conn.Conn
}
type http2StateConn struct { type http2StateConn struct {
*http2Conn *http2Conn
} }

View file

@ -288,14 +288,9 @@ type Server struct {
// ServeHTTP is the entry point for all HTTP requests. // ServeHTTP is the entry point for all HTTP requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil. // If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil.
// TODO: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged.
if r.TLS == nil { if r.TLS == nil {
// not all requests have a conn (like virtual requests) - see #5698 if tlsConnStateFunc, ok := r.Context().Value(tlsConnectionStateFuncCtxKey).(func() *tls.ConnectionState); ok {
if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok { r.TLS = tlsConnStateFunc()
if csc, ok := conn.(connectionStater); ok {
r.TLS = new(tls.ConnectionState)
*r.TLS = csc.ConnectionState()
}
} }
} }
@ -1115,11 +1110,14 @@ const (
// originally came into the server's entry handler // originally came into the server's entry handler
OriginalRequestCtxKey caddy.CtxKey = "original_request" OriginalRequestCtxKey caddy.CtxKey = "original_request"
// For referencing underlying net.Conn // DEPRECATED: not used anymore.
// This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin // To refer to the underlying connection, implement a middleware plugin
// that RegisterConnContext during provisioning. // that RegisterConnContext during provisioning.
ConnCtxKey caddy.CtxKey = "conn" ConnCtxKey caddy.CtxKey = "conn"
// used to get the tls connection state in the context, if available
tlsConnectionStateFuncCtxKey caddy.CtxKey = "tls_connection_state_func"
// For tracking whether the client is a trusted proxy // For tracking whether the client is a trusted proxy
TrustedProxyVarKey string = "trusted_proxy" TrustedProxyVarKey string = "trusted_proxy"