Add integration tests for multi-hop, gRPC trailers, and HTTP/1.1

Multi-hop integration tests (server package):
- TestMultiHopProxyProtocol: full edge→origin deployment with two
  mc-proxy instances. Edge uses L4 passthrough with send_proxy_protocol,
  origin has proxy_protocol listener with L7 route. Verifies the real
  client IP (127.0.0.1) flows through PROXY protocol into the origin's
  X-Forwarded-For header on the h2c backend.
- TestMultiHopFirewallBlocksRealIP: origin firewall blocks an IP from
  the PROXY header while allowing the TCP peer (edge proxy). Verifies
  the backend is never reached.

L7 package integration tests:
- TestL7LargeResponse: 1 MB response through the reverse proxy.
- TestL7GRPCTrailers: HTTP/2 trailer propagation (Grpc-Status,
  Grpc-Message) through the reverse proxy, validating gRPC
  compatibility.
- TestL7HTTP11Fallback: client negotiates HTTP/1.1 only (no h2 ALPN),
  verifies the proxy falls back to HTTP/1.1 serving and still
  forwards to the h2c backend successfully.

Also updates PROGRESS.md to mark all five phases complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 14:02:45 -07:00
parent 498f040cbe
commit b6c96ad42f
3 changed files with 408 additions and 14 deletions

View File

@@ -1052,6 +1052,234 @@ func TestProxyProtocolFirewallUsesRealIP(t *testing.T) {
wg.Wait()
}
// --- Multi-hop integration tests ---
func TestMultiHopProxyProtocol(t *testing.T) {
// Simulates edge → origin deployment with PROXY protocol.
//
// Client → [edge proxy] → PROXY v2 + TLS → [origin proxy] → h2c backend
// proxy_protocol=true
// L7 route
certPath, keyPath := testCert(t, "multihop.test")
// h2c backend on origin that echoes the X-Forwarded-For.
backendAddr := startH2CBackend(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "xff=%s", r.Header.Get("X-Forwarded-For"))
}))
// Origin proxy: proxy_protocol=true listener, L7 route to backend.
originLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("origin listen: %v", err)
}
originAddr := originLn.Addr().String()
originLn.Close()
originFw, _ := firewall.New("", nil, nil, nil, 0, 0)
originCfg := &config.Config{
Proxy: config.Proxy{
ConnectTimeout: config.Duration{Duration: 5 * time.Second},
IdleTimeout: config.Duration{Duration: 30 * time.Second},
ShutdownTimeout: config.Duration{Duration: 5 * time.Second},
},
}
originLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
originSrv := New(originCfg, originFw, []ListenerData{
{
ID: 1,
Addr: originAddr,
ProxyProtocol: true,
Routes: map[string]RouteInfo{
"multihop.test": {
Backend: backendAddr,
Mode: "l7",
TLSCert: certPath,
TLSKey: keyPath,
},
},
},
}, originLogger, "origin-test")
originCtx, originCancel := context.WithCancel(context.Background())
var originWg sync.WaitGroup
originWg.Add(1)
go func() {
defer originWg.Done()
originSrv.Run(originCtx)
}()
time.Sleep(50 * time.Millisecond)
defer func() {
originCancel()
originWg.Wait()
}()
// Edge proxy: L4 passthrough with send_proxy_protocol=true to origin.
edgeLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("edge listen: %v", err)
}
edgeAddr := edgeLn.Addr().String()
edgeLn.Close()
edgeFw, _ := firewall.New("", nil, nil, nil, 0, 0)
edgeSrv := New(originCfg, edgeFw, []ListenerData{
{
ID: 2,
Addr: edgeAddr,
Routes: map[string]RouteInfo{
"multihop.test": {
Backend: originAddr,
Mode: "l4",
SendProxyProtocol: true,
},
},
},
}, originLogger, "edge-test")
edgeCtx, edgeCancel := context.WithCancel(context.Background())
var edgeWg sync.WaitGroup
edgeWg.Add(1)
go func() {
defer edgeWg.Done()
edgeSrv.Run(edgeCtx)
}()
time.Sleep(50 * time.Millisecond)
defer func() {
edgeCancel()
edgeWg.Wait()
}()
// Client connects to edge proxy with TLS, as if it were a real client.
tlsConf := &tls.Config{
ServerName: "multihop.test",
InsecureSkipVerify: true,
NextProtos: []string{"h2"},
}
conn, err := tls.DialWithDialer(
&net.Dialer{Timeout: 5 * time.Second},
"tcp", edgeAddr, tlsConf,
)
if err != nil {
t.Fatalf("TLS dial edge: %v", err)
}
defer conn.Close()
tr := &http2.Transport{}
h2conn, err := tr.NewClientConn(conn)
if err != nil {
t.Fatalf("h2 client conn: %v", err)
}
req, _ := http.NewRequest("GET", "https://multihop.test/test", nil)
resp, err := h2conn.RoundTrip(req)
if err != nil {
t.Fatalf("RoundTrip: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
t.Fatalf("status = %d, want 200, body = %s", resp.StatusCode, body)
}
// The X-Forwarded-For should be 127.0.0.1 (the edge proxy's TCP client IP),
// carried through via PROXY protocol from edge to origin.
got := string(body)
if got != "xff=127.0.0.1" {
t.Fatalf("body = %q, want %q", got, "xff=127.0.0.1")
}
}
func TestMultiHopFirewallBlocksRealIP(t *testing.T) {
// Origin proxy with proxy_protocol=true and a firewall that blocks
// the real client IP. The TCP peer (edge proxy) is NOT blocked.
backendLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("backend listen: %v", err)
}
defer backendLn.Close()
reached := make(chan struct{}, 1)
go func() {
conn, err := backendLn.Accept()
if err != nil {
return
}
conn.Close()
reached <- struct{}{}
}()
originLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("origin listen: %v", err)
}
originAddr := originLn.Addr().String()
originLn.Close()
// Block 198.51.100.99 — this is the "real client IP" we'll put in the PROXY header.
originFw, _ := firewall.New("", []string{"198.51.100.99"}, nil, nil, 0, 0)
cfg := &config.Config{
Proxy: config.Proxy{
ConnectTimeout: config.Duration{Duration: 5 * time.Second},
IdleTimeout: config.Duration{Duration: 30 * time.Second},
ShutdownTimeout: config.Duration{Duration: 5 * time.Second},
},
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
originSrv := New(cfg, originFw, []ListenerData{
{
ID: 1,
Addr: originAddr,
ProxyProtocol: true,
Routes: map[string]RouteInfo{
"blocked.test": l4Route(backendLn.Addr().String()),
},
},
}, logger, "test")
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
originSrv.Run(ctx)
}()
time.Sleep(50 * time.Millisecond)
// Connect to origin and send PROXY header with the blocked IP.
conn, err := net.DialTimeout("tcp", originAddr, 2*time.Second)
if err != nil {
t.Fatalf("dial origin: %v", err)
}
defer conn.Close()
var ppBuf bytes.Buffer
proxyproto.WriteV2(&ppBuf,
netip.MustParseAddrPort("198.51.100.99:12345"),
netip.MustParseAddrPort("10.0.0.1:443"),
)
conn.Write(ppBuf.Bytes())
conn.Write(buildClientHello("blocked.test"))
// Connection should be dropped by firewall.
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
_, err = conn.Read(make([]byte, 1))
if err == nil {
t.Fatal("expected connection to be closed")
}
select {
case <-reached:
t.Fatal("backend was reached despite firewall block on real IP")
case <-time.After(200 * time.Millisecond):
}
cancel()
wg.Wait()
}
// --- L7 server integration tests ---
// testCert generates a self-signed TLS certificate for the given hostname.