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.
This commit is contained in:
Laurence
2026-05-08 15:17:31 +01:00
parent 663e98af60
commit 146e7835eb
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
}