From 5848c8d4b4d4687e3bbd6b29ccdf1c6e2e519d16 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 9 Apr 2026 16:04:11 -0400 Subject: [PATCH] Adjust to use data saved inside of the subnet rule --- clients/clients.go | 61 ++++++-- netstack2/handlers.go | 19 ++- netstack2/http_handler.go | 310 +++++++++++++++++++++---------------- netstack2/proxy.go | 45 +++--- netstack2/subnet_lookup.go | 26 ++-- netstack2/tun.go | 13 +- 6 files changed, 268 insertions(+), 206 deletions(-) diff --git a/clients/clients.go b/clients/clients.go index f1cf394..d57ab70 100644 --- a/clients/clients.go +++ b/clients/clients.go @@ -74,18 +74,18 @@ type PeerReading struct { } type WireGuardService struct { - interfaceName string - mtu int - client *websocket.Client - config WgConfig - key wgtypes.Key - newtId string - lastReadings map[string]PeerReading - mu sync.Mutex - Port uint16 - host string - serverPubKey string - token string + interfaceName string + mtu int + client *websocket.Client + config WgConfig + key wgtypes.Key + newtId string + lastReadings map[string]PeerReading + mu sync.Mutex + Port uint16 + host string + serverPubKey string + token string stopGetConfig func() pendingConfigChainId string // Netstack fields @@ -697,7 +697,14 @@ func (s *WireGuardService) syncTargets(desiredTargets []Target) error { }) } - s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId) + s.tnet.AddProxySubnetRule(netstack2.SubnetRule{ + SourcePrefix: sourcePrefix, + DestPrefix: destPrefix, + RewriteTo: target.RewriteTo, + PortRanges: portRanges, + DisableIcmp: target.DisableIcmp, + ResourceId: target.ResourceId, + }) logger.Info("Added target %s -> %s during sync", target.SourcePrefix, target.DestPrefix) } } @@ -819,7 +826,6 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { EnableTCPProxy: true, EnableUDPProxy: true, EnableICMPProxy: true, - EnableHTTPProxy: true, }, ) if err != nil { @@ -956,7 +962,14 @@ func (s *WireGuardService) ensureTargets(targets []Target) error { if err != nil { return fmt.Errorf("invalid CIDR %s: %v", sp, err) } - s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId) + s.tnet.AddProxySubnetRule(netstack2.SubnetRule{ + SourcePrefix: sourcePrefix, + DestPrefix: destPrefix, + RewriteTo: target.RewriteTo, + PortRanges: portRanges, + DisableIcmp: target.DisableIcmp, + ResourceId: target.ResourceId, + }) logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange) } } @@ -1349,7 +1362,14 @@ func (s *WireGuardService) handleAddTarget(msg websocket.WSMessage) { logger.Info("Invalid CIDR %s: %v", sp, err) continue } - s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId) + s.tnet.AddProxySubnetRule(netstack2.SubnetRule{ + SourcePrefix: sourcePrefix, + DestPrefix: destPrefix, + RewriteTo: target.RewriteTo, + PortRanges: portRanges, + DisableIcmp: target.DisableIcmp, + ResourceId: target.ResourceId, + }) logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange) } } @@ -1467,7 +1487,14 @@ func (s *WireGuardService) handleUpdateTarget(msg websocket.WSMessage) { logger.Info("Invalid CIDR %s: %v", sp, err) continue } - s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp, target.ResourceId) + s.tnet.AddProxySubnetRule(netstack2.SubnetRule{ + SourcePrefix: sourcePrefix, + DestPrefix: destPrefix, + RewriteTo: target.RewriteTo, + PortRanges: portRanges, + DisableIcmp: target.DisableIcmp, + ResourceId: target.ResourceId, + }) logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", sp, target.DestPrefix, target.RewriteTo, target.PortRange) } } diff --git a/netstack2/handlers.go b/netstack2/handlers.go index 8baa5e2..dabfee9 100644 --- a/netstack2/handlers.go +++ b/netstack2/handlers.go @@ -144,13 +144,18 @@ func (h *TCPHandler) handleTCPConn(netstackConn *gonet.TCPConn, id stack.Transpo 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) + // For HTTP/HTTPS ports, look up the matching subnet rule. If the rule has + // Protocol configured, hand the connection off to the HTTP handler which + // takes full ownership of the lifecycle (the defer close must not be + // installed before this point). + if (dstPort == 80 || dstPort == 443) && h.proxyHandler != nil && h.proxyHandler.httpHandler != nil { + srcAddr, _ := netip.ParseAddr(srcIP) + dstAddr, _ := netip.ParseAddr(dstIP) + rule := h.proxyHandler.subnetLookup.Match(srcAddr, dstAddr, dstPort, tcp.ProtocolNumber) + if rule != nil && rule.Protocol != "" { + logger.Info("TCP Forwarder: Routing %s:%d -> %s:%d to HTTP handler (%s)", + srcIP, srcPort, dstIP, dstPort, rule.Protocol) + h.proxyHandler.httpHandler.HandleConn(netstackConn, rule) return } } diff --git a/netstack2/http_handler.go b/netstack2/http_handler.go index 1502cf1..4efa6a1 100644 --- a/netstack2/http_handler.go +++ b/netstack2/http_handler.go @@ -6,6 +6,7 @@ package netstack2 import ( + "context" "crypto/tls" "fmt" "net" @@ -19,63 +20,52 @@ import ( ) // --------------------------------------------------------------------------- -// Hardcoded test configuration +// HTTPTarget // --------------------------------------------------------------------------- -// 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. +// HTTPTarget describes a single downstream HTTP or HTTPS service that the +// proxy should forward requests to. 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. +// --------------------------------------------------------------------------- +// HTTPHandler +// --------------------------------------------------------------------------- + +// HTTPHandler intercepts TCP connections from the netstack forwarder on ports +// 80 and 443 and services them as HTTP or HTTPS, reverse-proxying each request +// to downstream targets specified by the matching SubnetRule. // -// It is intentionally separate from TCPHandler: there is no overlap between -// raw-TCP connections and HTTP-aware connections on the same destination port. +// HTTP and raw TCP are fully separate: a connection is only routed here when +// its SubnetRule has Protocol set ("http" or "https"). All other connections +// on those ports fall through to the normal raw-TCP path. +// +// Incoming TLS termination (Protocol == "https") is performed per-connection +// using the certificate and key stored in the rule, so different subnet rules +// can present different certificates without sharing any state. +// +// Outbound connections to downstream targets honour HTTPTarget.UseHTTPS +// independently of the incoming protocol. 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 + + // proxyCache holds pre-built *httputil.ReverseProxy values keyed by the + // canonical target URL string ("scheme://host:port"). Building a proxy is + // cheap, but reusing one preserves the underlying http.Transport connection + // pool, which matters for throughput. + proxyCache sync.Map // map[string]*httputil.ReverseProxy + + // tlsCache holds pre-parsed *tls.Config values keyed by the concatenation + // of the PEM certificate and key. Parsing a keypair is relatively expensive + // and the same cert is likely reused across many connections. + tlsCache sync.Map // map[string]*tls.Config } // --------------------------------------------------------------------------- @@ -123,7 +113,7 @@ func (l *chanListener) Addr() net.Addr { } // send delivers conn to the listener. Returns false if the listener is already -// closed, in which case the caller should close conn itself. +// closed, in which case the caller is responsible for closing conn. func (l *chanListener) send(conn net.Conn) bool { select { case l.connCh <- conn: @@ -134,113 +124,96 @@ func (l *chanListener) send(conn net.Conn) bool { } // --------------------------------------------------------------------------- -// HTTPHandler constructor and lifecycle +// httpConnCtx – conn wrapper that carries a SubnetRule through the listener // --------------------------------------------------------------------------- -// NewHTTPHandler creates an HTTPHandler wired to the given stack and -// ProxyHandler, using the hardcoded test configuration defined at the top of -// this file. +// httpConnCtx wraps a net.Conn so the matching SubnetRule can be passed +// through the chanListener into the http.Server's ConnContext callback, +// making it available to request handlers via the request context. +type httpConnCtx struct { + net.Conn + rule *SubnetRule +} + +// connCtxKey is the unexported context key used to store a *SubnetRule on the +// per-connection context created by http.Server.ConnContext. +type connCtxKey struct{} + +// --------------------------------------------------------------------------- +// Constructor and lifecycle +// --------------------------------------------------------------------------- + +// NewHTTPHandler creates an HTTPHandler attached to the given stack and +// ProxyHandler. Call Start to begin serving connections. 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. +// Start launches the internal http.Server that services connections delivered +// via HandleConn. The server runs for the lifetime of the HTTPHandler; call +// Close to stop it. 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), + // ConnContext runs once per accepted connection and attaches the + // SubnetRule carried by httpConnCtx to the connection's context so + // that handleRequest can retrieve it without any global state. + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + if cc, ok := c.(*httpConnCtx); ok { + return context.WithValue(ctx, connCtxKey{}, cc.rule) + } + return ctx + }, } - 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) + go func() { + if err := h.server.Serve(h.listener); err != nil && err != http.ErrServerClosed { + logger.Error("HTTP handler: server exited unexpectedly: %v", 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)) - } + }() + logger.Info("HTTP handler: ready — routing determined per SubnetRule on ports 80/443") 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() +// HandleConn accepts a TCP connection from the netstack forwarder together +// with the SubnetRule that matched it. The HTTP handler takes full ownership +// of the connection's lifecycle; the caller must NOT close conn after this call. +// +// When rule.Protocol is "https", TLS termination is performed on conn using +// the certificate and key stored in rule.TLSCert and rule.TLSKey before the +// connection is passed to the HTTP server. The HTTP server itself is always +// plain-HTTP; TLS is fully unwrapped at this layer. +func (h *HTTPHandler) HandleConn(conn net.Conn, rule *SubnetRule) { + var effectiveConn net.Conn = conn + + if rule.Protocol == "https" { + tlsCfg, err := h.getTLSConfig(rule) + if err != nil { + logger.Error("HTTP handler: cannot build TLS config for connection from %s: %v", + conn.RemoteAddr(), err) + conn.Close() + return + } + // tls.Server wraps the raw conn; the TLS handshake is deferred until + // the first Read, which the http.Server will trigger naturally. + effectiveConn = tls.Server(conn, tlsCfg) + } + + wrapped := &httpConnCtx{Conn: effectiveConn, rule: rule} + if !h.listener.send(wrapped) { + // Listener is already closed — clean up the orphaned connection. + effectiveConn.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. +// Close gracefully shuts down the HTTP server and the underlying channel +// listener, causing the goroutine started in Start to exit. func (h *HTTPHandler) Close() error { if h.server != nil { if err := h.server.Close(); err != nil { @@ -254,23 +227,86 @@ func (h *HTTPHandler) Close() error { } // --------------------------------------------------------------------------- -// Request routing +// Internal helpers // --------------------------------------------------------------------------- -// 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. +// getTLSConfig returns a *tls.Config for the cert/key pair in rule, using a +// cache to avoid re-parsing the same keypair on every connection. +// The cache key is the concatenation of the PEM cert and key strings, so +// different rules that happen to share the same material hit the same entry. +func (h *HTTPHandler) getTLSConfig(rule *SubnetRule) (*tls.Config, error) { + cacheKey := rule.TLSCert + "|" + rule.TLSKey + if v, ok := h.tlsCache.Load(cacheKey); ok { + return v.(*tls.Config), nil + } + + cert, err := tls.X509KeyPair([]byte(rule.TLSCert), []byte(rule.TLSKey)) + if err != nil { + return nil, fmt.Errorf("failed to parse TLS keypair: %w", err) + } + cfg := &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + // LoadOrStore is safe under concurrent calls: if two goroutines race here + // both will produce a valid config; the loser's work is discarded. + actual, _ := h.tlsCache.LoadOrStore(cacheKey, cfg) + return actual.(*tls.Config), nil +} + +// getProxy returns a cached *httputil.ReverseProxy for the given target, +// creating one on first use. Reusing the proxy preserves its http.Transport +// connection pool, avoiding repeated TCP/TLS handshakes to the downstream. +func (h *HTTPHandler) getProxy(target HTTPTarget) *httputil.ReverseProxy { + scheme := "http" + if target.UseHTTPS { + scheme = "https" + } + cacheKey := fmt.Sprintf("%s://%s:%d", scheme, target.DestAddr, target.DestPort) + + if v, ok := h.proxyCache.Load(cacheKey); ok { + return v.(*httputil.ReverseProxy) + } + + targetURL := &url.URL{ + Scheme: scheme, + Host: fmt.Sprintf("%s:%d", target.DestAddr, target.DestPort), + } + proxy := httputil.NewSingleHostReverseProxy(targetURL) + + if target.UseHTTPS { + // Allow self-signed certificates on downstream HTTPS targets. + proxy.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // downstream self-signed certs are a supported configuration + }, + } + } + + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("HTTP handler: upstream error (%s %s -> %s): %v", + r.Method, r.URL.RequestURI(), cacheKey, err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + } + + actual, _ := h.proxyCache.LoadOrStore(cacheKey, proxy) + return actual.(*httputil.ReverseProxy) +} + +// handleRequest is the http.Handler entry point. It retrieves the SubnetRule +// attached to the connection by ConnContext, selects the first configured +// downstream target, and forwards the request via the cached ReverseProxy. +// +// TODO: add host/path-based routing across multiple HTTPTargets once the +// configuration model evolves beyond a single target per rule. func (h *HTTPHandler) handleRequest(w http.ResponseWriter, r *http.Request) { - if len(h.proxies) == 0 { - logger.Error("HTTP handler: no downstream targets configured") + rule, _ := r.Context().Value(connCtxKey{}).(*SubnetRule) + if rule == nil || len(rule.HTTPTargets) == 0 { + logger.Error("HTTP handler: no downstream targets for request %s %s", r.Method, r.URL.RequestURI()) 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] - + target := rule.HTTPTargets[0] scheme := "http" if target.UseHTTPS { scheme = "https" @@ -278,5 +314,5 @@ func (h *HTTPHandler) handleRequest(w http.ResponseWriter, r *http.Request) { logger.Info("HTTP handler: %s %s -> %s://%s:%d", r.Method, r.URL.RequestURI(), scheme, target.DestAddr, target.DestPort) - proxy.ServeHTTP(w, r) + h.getProxy(target).ServeHTTP(w, r) } \ No newline at end of file diff --git a/netstack2/proxy.go b/netstack2/proxy.go index 1ad469e..f4c2352 100644 --- a/netstack2/proxy.go +++ b/netstack2/proxy.go @@ -53,6 +53,14 @@ type SubnetRule struct { RewriteTo string // Optional rewrite address for DNAT - can be IP/CIDR or domain name PortRanges []PortRange // empty slice means all ports allowed ResourceId int // Optional resource ID from the server for access logging + + // HTTP proxy configuration (optional). + // When Protocol is non-empty the TCP connection is handled by HTTPHandler + // instead of the raw TCP forwarder. + Protocol string // "", "http", or "https" — controls the incoming (client-facing) protocol + HTTPTargets []HTTPTarget // downstream services to proxy requests to + TLSCert string // PEM-encoded certificate for incoming HTTPS termination + TLSKey string // PEM-encoded private key for incoming HTTPS termination } // GetAllRules returns a copy of all subnet rules @@ -132,13 +140,12 @@ 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 && !options.EnableHTTP { + if !options.EnableTCP && !options.EnableUDP && !options.EnableICMP { return nil, nil // No proxy needed } @@ -166,12 +173,21 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) { }), } - // Initialize TCP handler if enabled + // Initialize TCP handler if enabled. The HTTP handler piggybacks on the + // TCP forwarder — TCPHandler.handleTCPConn checks the subnet rule for + // ports 80/443 and routes matching connections to the HTTP handler, so + // the HTTP handler is always initialised alongside TCP. if options.EnableTCP { handler.tcpHandler = NewTCPHandler(handler.proxyStack, handler) if err := handler.tcpHandler.InstallTCPHandler(); err != nil { return nil, fmt.Errorf("failed to install TCP handler: %v", err) } + + 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") } // Initialize UDP handler if enabled @@ -191,17 +207,6 @@ 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") @@ -221,16 +226,14 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) { return handler, nil } -// AddSubnetRule adds a subnet with optional port restrictions to the proxy handler -// sourcePrefix: The IP prefix of the peer sending the data -// destPrefix: The IP prefix of the destination -// rewriteTo: Optional address to rewrite destination to - can be IP/CIDR or domain name -// If portRanges is nil or empty, all ports are allowed for this subnet -func (p *ProxyHandler) AddSubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool, resourceId int) { +// AddSubnetRule adds a subnet rule to the proxy handler. +// HTTP proxy behaviour is configured via rule.Protocol, rule.HTTPTargets, +// rule.TLSCert, and rule.TLSKey; leave Protocol empty for raw TCP/UDP. +func (p *ProxyHandler) AddSubnetRule(rule SubnetRule) { if p == nil || !p.enabled { return } - p.subnetLookup.AddSubnet(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp, resourceId) + p.subnetLookup.AddSubnet(rule) } // RemoveSubnetRule removes a subnet from the proxy handler diff --git a/netstack2/subnet_lookup.go b/netstack2/subnet_lookup.go index 317f85c..757908a 100644 --- a/netstack2/subnet_lookup.go +++ b/netstack2/subnet_lookup.go @@ -44,24 +44,18 @@ func prefixEqual(a, b netip.Prefix) bool { return a.Masked() == b.Masked() } -// AddSubnet adds a subnet rule with source and destination prefixes and optional port restrictions -// If portRanges is nil or empty, all ports are allowed for this subnet -// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com") -func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool, resourceId int) { +// AddSubnet adds a subnet rule to the lookup table. +// If rule.PortRanges is nil or empty, all ports are allowed. +// rule.RewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com"). +// HTTP proxy behaviour is driven by rule.Protocol, rule.HTTPTargets, rule.TLSCert, and rule.TLSKey. +func (sl *SubnetLookup) AddSubnet(rule SubnetRule) { sl.mu.Lock() defer sl.mu.Unlock() - rule := &SubnetRule{ - SourcePrefix: sourcePrefix, - DestPrefix: destPrefix, - DisableIcmp: disableIcmp, - RewriteTo: rewriteTo, - PortRanges: portRanges, - ResourceId: resourceId, - } + rulePtr := &rule // Canonicalize source prefix to handle host bits correctly - canonicalSourcePrefix := sourcePrefix.Masked() + canonicalSourcePrefix := rule.SourcePrefix.Masked() // Get or create destination trie for this source prefix destTriePtr, exists := sl.sourceTrie.Get(canonicalSourcePrefix) @@ -76,12 +70,12 @@ func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewrite // Canonicalize destination prefix to handle host bits correctly // BART masks prefixes internally, so we need to match that behavior in our bookkeeping - canonicalDestPrefix := destPrefix.Masked() + canonicalDestPrefix := rule.DestPrefix.Masked() // Add rule to destination trie // Original behavior: overwrite if same (sourcePrefix, destPrefix) exists // Store as single-element slice to match original overwrite behavior - destTriePtr.trie.Insert(canonicalDestPrefix, []*SubnetRule{rule}) + destTriePtr.trie.Insert(canonicalDestPrefix, []*SubnetRule{rulePtr}) // Update destTriePtr.rules - remove old rule with same canonical prefix if exists, then add new one // Use canonical comparison to handle cases like 10.0.0.5/24 vs 10.0.0.0/24 @@ -91,7 +85,7 @@ func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewrite newRules = append(newRules, r) } } - newRules = append(newRules, rule) + newRules = append(newRules, rulePtr) destTriePtr.rules = newRules } diff --git a/netstack2/tun.go b/netstack2/tun.go index e879d3b..5d2d6e1 100644 --- a/netstack2/tun.go +++ b/netstack2/tun.go @@ -59,7 +59,6 @@ type NetTunOptions struct { EnableTCPProxy bool EnableUDPProxy bool EnableICMPProxy bool - EnableHTTPProxy bool } // CreateNetTUN creates a new TUN device with netstack without proxying @@ -68,7 +67,6 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device, EnableTCPProxy: true, EnableUDPProxy: true, EnableICMPProxy: true, - EnableHTTPProxy: true, }) } @@ -95,7 +93,6 @@ 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 { @@ -354,13 +351,13 @@ func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) { return net.DialUDP(laddr, nil) } -// AddProxySubnetRule adds a subnet rule to the proxy handler -// If portRanges is nil or empty, all ports are allowed for this subnet -// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com") -func (net *Net) AddProxySubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool, resourceId int) { +// AddProxySubnetRule adds a subnet rule to the proxy handler. +// HTTP proxy behaviour is configured via rule.Protocol, rule.HTTPTargets, +// rule.TLSCert, and rule.TLSKey; leave Protocol empty for raw TCP/UDP. +func (net *Net) AddProxySubnetRule(rule SubnetRule) { tun := (*netTun)(net) if tun.proxyHandler != nil { - tun.proxyHandler.AddSubnetRule(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp, resourceId) + tun.proxyHandler.AddSubnetRule(rule) } }