fix(http): populate Request.TLS for private HTTPS via httpConnCtx

net/http only sets Request.TLS for *tls.Conn or conns implementing ConnectionState(). Our listener wrapped tls.Server in httpConnCtx with an embedded net.Conn, so TLS was never surfaced and r.TLS stayed nil. That triggered the HTTP→HTTPS permanent redirect on every request for HTTPS rules.

Add ConnectionState() on httpConnCtx delegating to the underlying TLS conn.
Add tests for TLS forwarding and plain TCP.


Former-commit-id: 146e7835eb
This commit is contained in:
Laurence
2026-05-08 15:17:31 +01:00
parent 6b42cac02d
commit f871b5e324
2 changed files with 63 additions and 0 deletions

View File

@@ -139,6 +139,21 @@ type httpConnCtx struct {
rule *SubnetRule
}
// ConnectionState allows net/http.Server to populate Request.TLS when the
// underlying connection is TLS (e.g. *tls.Conn from tls.Server). Without this,
// the connection is not *tls.Conn and does not expose ConnectionState through
// the net.Conn interface field, so tlsState stays nil and the HTTPS redirect
// in handleRequest runs on every request.
func (c *httpConnCtx) ConnectionState() tls.ConnectionState {
type tlsConn interface {
ConnectionState() tls.ConnectionState
}
if tc, ok := c.Conn.(tlsConn); ok {
return tc.ConnectionState()
}
return tls.ConnectionState{}
}
// connCtxKey is the unexported context key used to store a *SubnetRule on the
// per-connection context created by http.Server.ConnContext.
type connCtxKey struct{}

View File

@@ -0,0 +1,48 @@
package netstack2
import (
"crypto/tls"
"net"
"testing"
)
// tlsConnStub is a minimal net.Conn that also exposes TLS state, matching
// *tls.Conn's ConnectionState used by net/http.Server.
type tlsConnStub struct {
net.Conn
state tls.ConnectionState
}
func (t *tlsConnStub) ConnectionState() tls.ConnectionState {
return t.state
}
func TestHTTPConnCtxForwardsConnectionState(t *testing.T) {
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
inner := &tlsConnStub{
Conn: c1,
state: tls.ConnectionState{Version: tls.VersionTLS12, HandshakeComplete: true},
}
wrapped := &httpConnCtx{Conn: inner, rule: nil}
got := wrapped.ConnectionState()
if got.Version != tls.VersionTLS12 || !got.HandshakeComplete {
t.Fatalf("ConnectionState = %+v, want TLS 1.2 and HandshakeComplete", got)
}
}
func TestHTTPConnCtxConnectionStatePlainTCP(t *testing.T) {
c1, c2 := net.Pipe()
defer c1.Close()
defer c2.Close()
wrapped := &httpConnCtx{Conn: c1, rule: nil}
got := wrapped.ConnectionState()
if got.Version != 0 {
t.Fatalf("expected zero ConnectionState for plain conn, got %+v", got)
}
_ = c2
}