mirror of
https://github.com/fosrl/newt.git
synced 2026-04-10 20:06:38 +00:00
Adjust to use data saved inside of the subnet rule
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user