diff --git a/clients/clients.go b/clients/clients.go index 78bc0c3..f1cf394 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -819,6 +819,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { EnableTCPProxy: true, EnableUDPProxy: true, EnableICMPProxy: true, + EnableHTTPProxy: true, }, ) if err != nil { diff --git a/netstack2/handlers.go b/netstack2/handlers.go index 07c235f..8baa5e2 100644 --- a/netstack2/handlers.go +++ b/netstack2/handlers.go @@ -137,14 +137,26 @@ func (h *TCPHandler) InstallTCPHandler() error { // handleTCPConn handles a TCP connection by proxying it to the actual target func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.TransportEndpointID) { - defer netstackConn.Close() - - // Extract source and target address from the connection ID + // Extract source and target address from the connection ID first so they + // are available for HTTP routing before any defer is set up. srcIP := id.RemoteAddress.String() srcPort := id.RemotePort dstIP := id.LocalAddress.String() dstPort := id.LocalPort + // Route to the HTTP handler when the destination port belongs to it. + // The HTTP handler takes full ownership of the connection lifecycle, so we + // must NOT install the defer close before handing the conn off. + if h.proxyHandler != nil && h.proxyHandler.httpHandler != nil { + if h.proxyHandler.httpHandler.HandlesPort(dstPort) { + logger.Info("+++++++++++++++++++++++TCP Forwarder: Routing %s:%d -> %s:%d to HTTP handler", srcIP, srcPort, dstIP, dstPort) + h.proxyHandler.httpHandler.HandleConn(netstackConn) + return + } + } + + defer netstackConn.Close() + logger.Info("TCP Forwarder: Handling connection %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort) // Check if there's a destination rewrite for this connection (e.g., localhost targets) diff --git a/netstack2/http_handler.go b/netstack2/http_handler.go new file mode 100644 index 0000000..1502cf1 --- /dev/null +++ b/netstack2/http_handler.go @@ -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) +} \ No newline at end of file diff --git a/netstack2/proxy.go b/netstack2/proxy.go index e383fc0..1ad469e 100644 --- a/netstack2/proxy.go +++ b/netstack2/proxy.go @@ -114,6 +114,7 @@ type ProxyHandler struct { tcpHandler *TCPHandler udpHandler *UDPHandler icmpHandler *ICMPHandler + httpHandler *HTTPHandler subnetLookup *SubnetLookup natTable map[connKey]*natState reverseNatTable map[reverseConnKey]*natState // Reverse lookup map for O(1) reply packet NAT @@ -131,12 +132,13 @@ type ProxyHandlerOptions struct { EnableTCP bool EnableUDP bool EnableICMP bool + EnableHTTP bool MTU int } // NewProxyHandler creates a new proxy handler for promiscuous mode func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) { - if !options.EnableTCP && !options.EnableUDP && !options.EnableICMP { + if !options.EnableTCP && !options.EnableUDP && !options.EnableICMP && !options.EnableHTTP { return nil, nil // No proxy needed } @@ -189,6 +191,17 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) { logger.Debug("ProxyHandler: ICMP handler enabled") } + // Initialize HTTP handler if enabled. The HTTP handler piggybacks on the + // TCP forwarder: TCPHandler.handleTCPConn checks HandlesPort() and routes + // matching connections here instead of doing raw byte forwarding. + if options.EnableHTTP { + handler.httpHandler = NewHTTPHandler(handler.proxyStack, handler) + if err := handler.httpHandler.Start(); err != nil { + return nil, fmt.Errorf("failed to start HTTP handler: %v", err) + } + logger.Debug("ProxyHandler: HTTP handler enabled") + } + // // Example 1: Add a rule with no port restrictions (all ports allowed) // // This accepts all traffic FROM 10.0.0.0/24 TO 10.20.20.0/24 // sourceSubnet := netip.MustParsePrefix("10.0.0.0/24") @@ -794,6 +807,11 @@ func (p *ProxyHandler) Close() error { p.accessLogger.Close() } + // Shut down HTTP handler + if p.httpHandler != nil { + p.httpHandler.Close() + } + // Close ICMP replies channel if p.icmpReplies != nil { close(p.icmpReplies) diff --git a/netstack2/tun.go b/netstack2/tun.go index 3183c36..e879d3b 100644 --- a/netstack2/tun.go +++ b/netstack2/tun.go @@ -59,6 +59,7 @@ type NetTunOptions struct { EnableTCPProxy bool EnableUDPProxy bool EnableICMPProxy bool + EnableHTTPProxy bool } // CreateNetTUN creates a new TUN device with netstack without proxying @@ -67,6 +68,7 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, EnableTCPProxy: true, EnableUDPProxy: true, EnableICMPProxy: true, + EnableHTTPProxy: true, }) } @@ -93,6 +95,7 @@ func CreateNetTUNWithOptions(localAddresses, dnsServers []netip.Addr, mtu int, o EnableTCP: options.EnableTCPProxy, EnableUDP: options.EnableUDPProxy, EnableICMP: options.EnableICMPProxy, + EnableHTTP: options.EnableHTTPProxy, MTU: mtu, }) if err != nil {