Files
newt/netstack2/http_handler.go
2026-04-09 11:43:26 -04:00

282 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* SPDX-License-Identifier: MIT
*
* Copyright (C) 2017-2025 WireGuard LLC. All Rights Reserved.
*/
package netstack2
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"github.com/fosrl/newt/logger"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
// ---------------------------------------------------------------------------
// Hardcoded test configuration
// ---------------------------------------------------------------------------
// testHTTPServeHTTPS controls whether the proxy presents HTTP or HTTPS to
// incoming connections. Flip to true and supply valid cert/key paths to test
// TLS termination.
const testHTTPServeHTTPS = false
// testHTTPCertFile / testHTTPKeyFile are paths to a self-signed certificate
// used when testHTTPServeHTTPS == true.
const testHTTPCertFile = "/tmp/test-cert.pem"
const testHTTPKeyFile = "/tmp/test-key.pem"
// testHTTPListenPort is the destination port the handler intercepts from the
// netstack TCP forwarder (e.g. 80 for plain HTTP, 443 for HTTPS termination).
const testHTTPListenPort uint16 = 80
// testHTTPTargets is the hardcoded list of downstream services used for
// testing. DestAddr / DestPort describe where the real HTTP(S) server lives;
// UseHTTPS controls whether the outbound leg uses TLS.
var testHTTPTargets = []HTTPTarget{
{DestAddr: "127.0.0.1", DestPort: 8080, UseHTTPS: false},
}
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
// HTTPTarget describes a single downstream HTTP or HTTPS service.
type HTTPTarget struct {
DestAddr string // IP address or hostname of the downstream service
DestPort uint16 // TCP port of the downstream service
UseHTTPS bool // When true the outbound leg uses HTTPS
}
// HTTPHandler intercepts TCP connections from the netstack forwarder and
// services them as HTTP or HTTPS, reverse-proxying each request to one of the
// configured downstream HTTPTarget services.
//
// It is intentionally separate from TCPHandler: there is no overlap between
// raw-TCP connections and HTTP-aware connections on the same destination port.
type HTTPHandler struct {
stack *stack.Stack
proxyHandler *ProxyHandler
// Configuration (populated from hardcoded test values by NewHTTPHandler).
targets []HTTPTarget
listenPort uint16 // Port this handler claims; used for routing by TCPHandler
serveHTTPS bool // Present TLS to the incoming (client) side
certFile string // PEM certificate for the incoming TLS listener
keyFile string // PEM private key for the incoming TLS listener
// Runtime state initialised by Start().
listener *chanListener
server *http.Server
// One pre-built reverse proxy per target entry.
proxies []*httputil.ReverseProxy
}
// ---------------------------------------------------------------------------
// chanListener net.Listener backed by a channel
// ---------------------------------------------------------------------------
// chanListener implements net.Listener by receiving net.Conn values over a
// buffered channel. This lets the netstack TCP forwarder hand off connections
// directly to a running http.Server without any real OS socket.
type chanListener struct {
connCh chan net.Conn
closed chan struct{}
once sync.Once
}
func newChanListener() *chanListener {
return &chanListener{
connCh: make(chan net.Conn, 128),
closed: make(chan struct{}),
}
}
// Accept blocks until a connection is available or the listener is closed.
func (l *chanListener) Accept() (net.Conn, error) {
select {
case conn, ok := <-l.connCh:
if !ok {
return nil, net.ErrClosed
}
return conn, nil
case <-l.closed:
return nil, net.ErrClosed
}
}
// Close shuts down the listener; subsequent Accept calls return net.ErrClosed.
func (l *chanListener) Close() error {
l.once.Do(func() { close(l.closed) })
return nil
}
// Addr returns a placeholder address (the listener has no real OS socket).
func (l *chanListener) Addr() net.Addr {
return &net.TCPAddr{}
}
// send delivers conn to the listener. Returns false if the listener is already
// closed, in which case the caller should close conn itself.
func (l *chanListener) send(conn net.Conn) bool {
select {
case l.connCh <- conn:
return true
case <-l.closed:
return false
}
}
// ---------------------------------------------------------------------------
// HTTPHandler constructor and lifecycle
// ---------------------------------------------------------------------------
// NewHTTPHandler creates an HTTPHandler wired to the given stack and
// ProxyHandler, using the hardcoded test configuration defined at the top of
// this file.
func NewHTTPHandler(s *stack.Stack, ph *ProxyHandler) *HTTPHandler {
return &HTTPHandler{
stack: s,
proxyHandler: ph,
targets: testHTTPTargets,
listenPort: testHTTPListenPort,
serveHTTPS: testHTTPServeHTTPS,
certFile: testHTTPCertFile,
keyFile: testHTTPKeyFile,
}
}
// Start builds the per-target reverse proxies and launches the HTTP(S) server
// that will service connections delivered via HandleConn.
func (h *HTTPHandler) Start() error {
// Build one ReverseProxy per target.
h.proxies = make([]*httputil.ReverseProxy, 0, len(h.targets))
for i, t := range h.targets {
scheme := "http"
if t.UseHTTPS {
scheme = "https"
}
targetURL := &url.URL{
Scheme: scheme,
Host: fmt.Sprintf("%s:%d", t.DestAddr, t.DestPort),
}
proxy := httputil.NewSingleHostReverseProxy(targetURL)
// For HTTPS downstream, allow self-signed certificates during testing.
if t.UseHTTPS {
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // intentional for test targets
},
}
}
idx := i // capture for closure
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("HTTP handler: upstream error (target %d, %s %s): %v",
idx, r.Method, r.URL.RequestURI(), err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
h.proxies = append(h.proxies, proxy)
}
h.listener = newChanListener()
h.server = &http.Server{
Handler: http.HandlerFunc(h.handleRequest),
}
if h.serveHTTPS {
cert, err := tls.LoadX509KeyPair(h.certFile, h.keyFile)
if err != nil {
return fmt.Errorf("HTTP handler: failed to load TLS keypair (%s, %s): %w",
h.certFile, h.keyFile, err)
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
}
tlsListener := tls.NewListener(h.listener, tlsCfg)
go func() {
if err := h.server.Serve(tlsListener); err != nil && err != http.ErrServerClosed {
logger.Error("HTTP handler: HTTPS server exited: %v", err)
}
}()
logger.Info("HTTP handler: listening (HTTPS) on port %d, %d downstream target(s)",
h.listenPort, len(h.targets))
} else {
go func() {
if err := h.server.Serve(h.listener); err != nil && err != http.ErrServerClosed {
logger.Error("HTTP handler: HTTP server exited: %v", err)
}
}()
logger.Info("HTTP handler: listening (HTTP) on port %d, %d downstream target(s)",
h.listenPort, len(h.targets))
}
return nil
}
// HandleConn accepts a TCP connection from the netstack forwarder and delivers
// it to the running HTTP(S) server. The HTTP handler takes full ownership of
// the connection's lifecycle; the caller must NOT close conn after this call.
func (h *HTTPHandler) HandleConn(conn net.Conn) {
if !h.listener.send(conn) {
// Listener already closed clean up the orphaned connection.
conn.Close()
}
}
// HandlesPort reports whether this handler is responsible for connections
// arriving on the given destination port.
func (h *HTTPHandler) HandlesPort(port uint16) bool {
return port == h.listenPort
}
// Close shuts down the underlying HTTP server and the channel listener.
func (h *HTTPHandler) Close() error {
if h.server != nil {
if err := h.server.Close(); err != nil {
return err
}
}
if h.listener != nil {
h.listener.Close()
}
return nil
}
// ---------------------------------------------------------------------------
// Request routing
// ---------------------------------------------------------------------------
// handleRequest proxies an incoming HTTP request to the appropriate downstream
// target. Currently always routes to the first (and, in the hardcoded test
// setup, only) configured target.
func (h *HTTPHandler) handleRequest(w http.ResponseWriter, r *http.Request) {
if len(h.proxies) == 0 {
logger.Error("HTTP handler: no downstream targets configured")
http.Error(w, "no targets configured", http.StatusBadGateway)
return
}
// TODO: add host/path-based routing when moving beyond hardcoded test config.
proxy := h.proxies[0]
target := h.targets[0]
scheme := "http"
if target.UseHTTPS {
scheme = "https"
}
logger.Info("HTTP handler: %s %s -> %s://%s:%d",
r.Method, r.URL.RequestURI(), scheme, target.DestAddr, target.DestPort)
proxy.ServeHTTP(w, r)
}