Basic http is working

This commit is contained in:
Owen
2026-04-09 11:43:26 -04:00
parent 7e1e3408d5
commit 47c646bc33
5 changed files with 320 additions and 4 deletions

282
netstack2/http_handler.go Normal file
View File

@@ -0,0 +1,282 @@
/* 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)
}