Files
mc-proxy/internal/l7/prefixconn_test.go
Kyle Isom 97909b7fbc Add L7 TLS-terminating HTTP/2 reverse proxy
New internal/l7 package implements TLS termination and HTTP/2 reverse
proxying for L7 routes. The proxy terminates the client TLS connection
using per-route certificates, then forwards HTTP/2 traffic to backends
over h2c (plaintext HTTP/2) or h2 (re-encrypted TLS).

PrefixConn replays the peeked ClientHello bytes into crypto/tls.Server
so the TLS handshake sees the complete ClientHello despite SNI
extraction having already read it.

Serve() is the L7 entry point: TLS handshake with route certificate,
ALPN negotiation (h2 preferred, HTTP/1.1 fallback), then HTTP reverse
proxy via httputil.ReverseProxy. Backend transport uses h2c by default
(AllowHTTP + plain TCP dial) or h2-over-TLS when backend_tls is set.

Forwarding headers (X-Forwarded-For, X-Forwarded-Proto, X-Real-IP)
are injected from the real client IP in the Rewrite function. PROXY
protocol v2 is sent to backends when send_proxy_protocol is enabled,
using the request context to carry the client address through the
HTTP/2 transport's dial function.

Server integration: handleConn dispatches to handleL7 when route.Mode
is "l7". The L7 handler converts RouteInfo to l7.RouteConfig and
delegates to l7.Serve.

L7 package tests: PrefixConn (4 tests), h2c backend round-trip,
forwarding header injection, backend unreachable (502), multiple
HTTP/2 requests over one connection.

Server integration tests: L7 route through full server pipeline with
TLS client, mixed L4+L7 routes on the same listener verifying both
paths work independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:43:20 -07:00

155 lines
3.0 KiB
Go

package l7
import (
"io"
"net"
"testing"
"time"
)
func TestPrefixConnRead(t *testing.T) {
// Create a TCP pair.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
conn.Write([]byte("WORLD"))
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
pc := NewPrefixConn(conn, []byte("HELLO"))
// Read all data: should get "HELLOWORLD".
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
all, err := io.ReadAll(pc)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(all) != "HELLOWORLD" {
t.Fatalf("got %q, want %q", all, "HELLOWORLD")
}
}
func TestPrefixConnSmallReads(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
conn.Write([]byte("CD"))
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
pc := NewPrefixConn(conn, []byte("AB"))
// Read 1 byte at a time from the prefix.
buf := make([]byte, 1)
n, err := pc.Read(buf)
if err != nil || n != 1 || buf[0] != 'A' {
t.Fatalf("first read: n=%d, err=%v, buf=%q", n, err, buf[:n])
}
n, err = pc.Read(buf)
if err != nil || n != 1 || buf[0] != 'B' {
t.Fatalf("second read: n=%d, err=%v, buf=%q", n, err, buf[:n])
}
// Now reads come from the underlying conn.
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
rest, err := io.ReadAll(pc)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(rest) != "CD" {
t.Fatalf("got %q, want %q", rest, "CD")
}
}
func TestPrefixConnEmptyPrefix(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
conn.Write([]byte("DATA"))
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
pc := NewPrefixConn(conn, nil)
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
all, err := io.ReadAll(pc)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(all) != "DATA" {
t.Fatalf("got %q, want %q", all, "DATA")
}
}
func TestPrefixConnDelegates(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
go func() {
conn, _ := ln.Accept()
if conn != nil {
conn.Close()
}
}()
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
pc := NewPrefixConn(conn, []byte("X"))
// RemoteAddr, LocalAddr should delegate.
if pc.RemoteAddr() == nil {
t.Fatal("RemoteAddr returned nil")
}
if pc.LocalAddr() == nil {
t.Fatal("LocalAddr returned nil")
}
}