use embedded netbird agent for tunneling

This commit is contained in:
pascal
2026-01-15 17:03:27 +01:00
parent ed5f98da5b
commit 7527e0ebdb
10 changed files with 116 additions and 186 deletions

View File

@@ -52,7 +52,7 @@ func run(cmd *cobra.Command, args []string) error {
log.Infof("Starting Netbird Proxy - %s", version.Short()) log.Infof("Starting Netbird Proxy - %s", version.Short())
log.Debugf("Full version info: %s", version.String()) log.Debugf("Full version info: %s", version.String())
log.Info("Configuration loaded successfully") log.Info("Configuration loaded successfully")
log.Infof("Listen Address: %s", config.ListenAddress) log.Infof("Listen Address: %s", config.ReverseProxy.ListenAddress)
log.Infof("Log Level: %s", config.LogLevel) log.Infof("Log Level: %s", config.LogLevel)
// Create server instance // Create server instance

View File

@@ -1,5 +1,4 @@
{ {
"listen_address": ":443",
"read_timeout": "30s", "read_timeout": "30s",
"write_timeout": "30s", "write_timeout": "30s",
"idle_timeout": "60s", "idle_timeout": "60s",
@@ -8,20 +7,24 @@
"grpc_listen_address": ":50051", "grpc_listen_address": ":50051",
"proxy_id": "proxy-1", "proxy_id": "proxy-1",
"enable_grpc": true, "enable_grpc": true,
"http_listen_address": ":80", "reverse_proxy": {
"enable_https": false, "listen_address": ":443",
"tls_email": "your-email@example.com", "management_url": "https://api.netbird.io",
"cert_cache_dir": "./certs", "http_listen_address": ":80",
"oidc_config": { "enable_https": false,
"provider_url": "https://your-oidc-provider.com", "tls_email": "your-email@example.com",
"client_id": "your-client-id", "cert_cache_dir": "./certs",
"client_secret": "your-client-secret-if-needed", "oidc_config": {
"redirect_url": "http://localhost:80/auth/callback", "provider_url": "https://your-oidc-provider.com",
"scopes": ["openid", "profile", "email"], "client_id": "your-client-id",
"jwt_keys_location": "https://your-oidc-provider.com/.well-known/jwks.json", "client_secret": "your-client-secret-if-needed",
"jwt_issuer": "https://your-oidc-provider.com/", "redirect_url": "http://localhost:80/auth/callback",
"jwt_audience": ["your-api-identifier-or-client-id"], "scopes": ["openid", "profile", "email"],
"jwt_idp_signkey_refresh_enabled": false, "jwt_keys_location": "https://your-oidc-provider.com/.well-known/jwks.json",
"session_cookie_name": "auth_session" "jwt_issuer": "https://your-oidc-provider.com/",
"jwt_audience": ["your-api-identifier-or-client-id"],
"jwt_idp_signkey_refresh_enabled": false,
"session_cookie_name": "auth_session"
}
} }
} }

View File

@@ -1,10 +1,10 @@
package reverseproxy package reverseproxy
import ( import (
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"github.com/netbirdio/netbird/client/embed"
"github.com/netbirdio/netbird/proxy/internal/auth" "github.com/netbirdio/netbird/proxy/internal/auth"
"github.com/netbirdio/netbird/proxy/internal/auth/oidc" "github.com/netbirdio/netbird/proxy/internal/auth/oidc"
) )
@@ -12,28 +12,28 @@ import (
// Config holds the reverse proxy configuration // Config holds the reverse proxy configuration
type Config struct { type Config struct {
// ListenAddress is the address to listen on for HTTPS (default ":443") // ListenAddress is the address to listen on for HTTPS (default ":443")
ListenAddress string ListenAddress string `env:"NB_REVERSE_PROXY_LISTEN_ADDRESS" envDefault:":443" json:"listen_address"`
// ManagementURL is the URL of the management server
ManagementURL string `env:"NB_REVERSE_PROXY_MANAGEMENT_URL" json:"management_url"`
// HTTPListenAddress is the address for HTTP (default ":80") // HTTPListenAddress is the address for HTTP (default ":80")
// Used for ACME challenges when HTTPS is enabled, or as main listener when HTTPS is disabled // Used for ACME challenges when HTTPS is enabled, or as main listener when HTTPS is disabled
HTTPListenAddress string HTTPListenAddress string `env:"NB_REVERSE_PROXY_HTTP_LISTEN_ADDRESS" envDefault:":80" json:"http_listen_address"`
// EnableHTTPS enables automatic HTTPS with Let's Encrypt // EnableHTTPS enables automatic HTTPS with Let's Encrypt
EnableHTTPS bool EnableHTTPS bool `env:"NB_REVERSE_PROXY_ENABLE_HTTPS" envDefault:"false" json:"enable_https"`
// TLSEmail is the email for Let's Encrypt registration // TLSEmail is the email for Let's Encrypt registration
TLSEmail string TLSEmail string `env:"NB_REVERSE_PROXY_TLS_EMAIL" json:"tls_email"`
// CertCacheDir is the directory to cache certificates (default "./certs") // CertCacheDir is the directory to cache certificates (default "./certs")
CertCacheDir string CertCacheDir string `env:"NB_REVERSE_PROXY_CERT_CACHE_DIR" envDefault:"./certs" json:"cert_cache_dir"`
// RequestDataCallback is called for each proxied request with metrics
RequestDataCallback RequestDataCallback
// OIDCConfig is the global OIDC/OAuth configuration for authentication // OIDCConfig is the global OIDC/OAuth configuration for authentication
// This is shared across all routes that use Bearer authentication // This is shared across all routes that use Bearer authentication
// If nil, routes with Bearer auth will fail to initialize // If nil, routes with Bearer auth will fail to initialize
OIDCConfig *oidc.Config OIDCConfig *oidc.Config `json:"oidc_config"`
} }
// RouteConfig defines a routing configuration // RouteConfig defines a routing configuration
@@ -50,10 +50,8 @@ type RouteConfig struct {
// Must have at least one entry. Use "/" or "" for the default/catch-all route. // Must have at least one entry. Use "/" or "" for the default/catch-all route.
PathMappings map[string]string PathMappings map[string]string
// Conn is the network connection to use for this route SetupKey string
// This allows routing through specific tunnels (e.g., WireGuard) per route nbClient *embed.Client
// This connection will be reused for all requests to this route
Conn net.Conn
// AuthConfig is optional authentication configuration for this route // AuthConfig is optional authentication configuration for this route
// Configure ONE of: BasicAuth, PIN, or Bearer (JWT/OIDC) // Configure ONE of: BasicAuth, PIN, or Bearer (JWT/OIDC)

View File

@@ -1,54 +0,0 @@
package reverseproxy
import (
"fmt"
"net"
"sync"
"time"
)
// defaultConn is a lazy connection wrapper that uses the standard network dialer
// This is useful for testing or development when not using WireGuard tunnels
type defaultConn struct {
dialer *net.Dialer
mu sync.Mutex
conns map[string]net.Conn // cache connections by "network:address"
}
func (dc *defaultConn) Read(b []byte) (n int, err error) {
return 0, fmt.Errorf("Read not supported on defaultConn - use dial via Transport")
}
func (dc *defaultConn) Write(b []byte) (n int, err error) {
return 0, fmt.Errorf("Write not supported on defaultConn - use dial via Transport")
}
func (dc *defaultConn) Close() error {
dc.mu.Lock()
defer dc.mu.Unlock()
for _, conn := range dc.conns {
conn.Close()
}
dc.conns = make(map[string]net.Conn)
return nil
}
func (dc *defaultConn) LocalAddr() net.Addr { return nil }
func (dc *defaultConn) RemoteAddr() net.Addr { return nil }
func (dc *defaultConn) SetDeadline(t time.Time) error { return nil }
func (dc *defaultConn) SetReadDeadline(t time.Time) error { return nil }
func (dc *defaultConn) SetWriteDeadline(t time.Time) error { return nil }
// NewDefaultConn creates a connection wrapper that uses the standard network dialer
// This is useful for testing or development when not using WireGuard tunnels
// The actual dialing happens when the HTTP Transport calls DialContext
func NewDefaultConn() net.Conn {
return &defaultConn{
dialer: &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
conns: make(map[string]net.Conn),
}
}

View File

@@ -1,8 +1,6 @@
package reverseproxy package reverseproxy
import ( import (
"context"
"net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@@ -187,32 +185,15 @@ func (p *Proxy) createProxy(routeConfig *RouteConfig, target string) *httputil.R
// Create reverse proxy // Create reverse proxy
proxy := httputil.NewSingleHostReverseProxy(targetURL) proxy := httputil.NewSingleHostReverseProxy(targetURL)
// Check if this is a defaultConn (for testing) // Configure transport to use the provided connection (WireGuard, etc.)
if dc, ok := routeConfig.Conn.(*defaultConn); ok { proxy.Transport = &http.Transport{
// For defaultConn, use its dialer directly DialContext: routeConfig.nbClient.DialContext,
proxy.Transport = &http.Transport{ MaxIdleConns: 1,
DialContext: dc.dialer.DialContext, MaxIdleConnsPerHost: 1,
MaxIdleConns: 100, IdleConnTimeout: 0, // Keep alive indefinitely
IdleConnTimeout: 90 * time.Second, DisableKeepAlives: false,
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
}
log.Infof("Using default network dialer for route %s (testing mode)", routeConfig.ID)
} else {
// Configure transport to use the provided connection (WireGuard, etc.)
proxy.Transport = &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
log.Debugf("Using custom connection for route %s to %s", routeConfig.ID, address)
return routeConfig.Conn, nil
},
MaxIdleConns: 1,
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 0, // Keep alive indefinitely
DisableKeepAlives: false,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
log.Infof("Using custom connection for route %s", routeConfig.ID)
} }
// Custom error handler // Custom error handler

View File

@@ -49,10 +49,9 @@ func New(config Config) (*Proxy, error) {
} }
p := &Proxy{ p := &Proxy{
config: config, config: config,
routes: make(map[string]*RouteConfig), routes: make(map[string]*RouteConfig),
isRunning: false, isRunning: false,
requestCallback: config.RequestDataCallback,
} }
// Initialize OIDC handler if OIDC is configured // Initialize OIDC handler if OIDC is configured
@@ -65,6 +64,13 @@ func New(config Config) (*Proxy, error) {
return p, nil return p, nil
} }
// SetRequestCallback sets the callback for request metrics
func (p *Proxy) SetRequestCallback(callback RequestDataCallback) {
p.mu.Lock()
defer p.mu.Unlock()
p.requestCallback = callback
}
// GetConfig returns the proxy configuration // GetConfig returns the proxy configuration
func (p *Proxy) GetConfig() Config { func (p *Proxy) GetConfig() Config {
return p.config return p.config

View File

@@ -1,9 +1,17 @@
package reverseproxy package reverseproxy
import ( import (
"context"
"fmt" "fmt"
"time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/embed"
)
const (
clientStartupTimeout = 30 * time.Second
) )
// AddRoute adds a new route to the proxy // AddRoute adds a new route to the proxy
@@ -20,8 +28,8 @@ func (p *Proxy) AddRoute(route *RouteConfig) error {
if len(route.PathMappings) == 0 { if len(route.PathMappings) == 0 {
return fmt.Errorf("route must have at least one path mapping") return fmt.Errorf("route must have at least one path mapping")
} }
if route.Conn == nil { if route.SetupKey == "" {
return fmt.Errorf("route connection (Conn) is required") return fmt.Errorf("route setup key is required")
} }
p.mu.Lock() p.mu.Lock()
@@ -32,6 +40,19 @@ func (p *Proxy) AddRoute(route *RouteConfig) error {
return fmt.Errorf("route for domain %s already exists", route.Domain) return fmt.Errorf("route for domain %s already exists", route.Domain)
} }
client, err := embed.New(embed.Options{DeviceName: fmt.Sprintf("ingress-%s", route.ID), ManagementURL: p.config.ManagementURL, SetupKey: route.SetupKey})
if err != nil {
return fmt.Errorf("failed to create embedded client for route %s: %v", route.ID, err)
}
ctx, _ := context.WithTimeout(context.Background(), clientStartupTimeout)
err = client.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start embedded client for route %s: %v", route.ID, err)
}
route.nbClient = client
// Add route with domain as key // Add route with domain as key
p.routes[route.Domain] = route p.routes[route.Domain] = route

View File

@@ -9,7 +9,7 @@ import (
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
) )
// Start starts the reverse proxy server // Start starts the reverse proxy server (non-blocking)
func (p *Proxy) Start() error { func (p *Proxy) Start() error {
p.mu.Lock() p.mu.Lock()
if p.isRunning { if p.isRunning {
@@ -29,7 +29,7 @@ func (p *Proxy) Start() error {
return p.startHTTP(handler) return p.startHTTP(handler)
} }
// startHTTPS starts the proxy with HTTPS and Let's Encrypt // startHTTPS starts the proxy with HTTPS and Let's Encrypt (non-blocking)
func (p *Proxy) startHTTPS(handler http.Handler) error { func (p *Proxy) startHTTPS(handler http.Handler) error {
// Setup autocert manager with dynamic host policy // Setup autocert manager with dynamic host policy
p.autocertManager = &autocert.Manager{ p.autocertManager = &autocert.Manager{
@@ -53,32 +53,36 @@ func (p *Proxy) startHTTPS(handler http.Handler) error {
} }
}() }()
// Start HTTPS server // Start HTTPS server in background
p.server = &http.Server{ p.server = &http.Server{
Addr: p.config.ListenAddress, Addr: p.config.ListenAddress,
Handler: handler, Handler: handler,
TLSConfig: p.autocertManager.TLSConfig(), TLSConfig: p.autocertManager.TLSConfig(),
} }
log.Infof("Starting HTTPS reverse proxy server on %s", p.config.ListenAddress) go func() {
if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { log.Infof("Starting HTTPS reverse proxy server on %s", p.config.ListenAddress)
return fmt.Errorf("HTTPS server failed: %w", err) if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
} log.Errorf("HTTPS server failed: %v", err)
}
}()
return nil return nil
} }
// startHTTP starts the proxy with HTTP only (no TLS) // startHTTP starts the proxy with HTTP only (non-blocking)
func (p *Proxy) startHTTP(handler http.Handler) error { func (p *Proxy) startHTTP(handler http.Handler) error {
p.server = &http.Server{ p.server = &http.Server{
Addr: p.config.HTTPListenAddress, Addr: p.config.HTTPListenAddress,
Handler: handler, Handler: handler,
} }
log.Infof("Starting HTTP reverse proxy server on %s (HTTPS disabled)", p.config.HTTPListenAddress) go func() {
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Infof("Starting HTTP reverse proxy server on %s (HTTPS disabled)", p.config.HTTPListenAddress)
return fmt.Errorf("HTTP server failed: %w", err) if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
} log.Errorf("HTTP server failed: %w", err)
}
}()
return nil return nil
} }

View File

@@ -10,7 +10,7 @@ import (
"github.com/caarlos0/env/v11" "github.com/caarlos0/env/v11"
"github.com/netbirdio/netbird/proxy/internal/auth/oidc" "github.com/netbirdio/netbird/proxy/internal/reverseproxy"
) )
var ( var (
@@ -48,9 +48,6 @@ func (d Duration) ToDuration() time.Duration {
// Config holds the configuration for the reverse proxy server // Config holds the configuration for the reverse proxy server
type Config struct { type Config struct {
// ListenAddress is the address the proxy server will listen on (e.g., ":443" or "0.0.0.0:443")
ListenAddress string `env:"NB_PROXY_LISTEN_ADDRESS" envDefault:":443" json:"listen_address"`
// ReadTimeout is the maximum duration for reading the entire request, including the body // ReadTimeout is the maximum duration for reading the entire request, including the body
ReadTimeout time.Duration `env:"NB_PROXY_READ_TIMEOUT" envDefault:"30s" json:"read_timeout"` ReadTimeout time.Duration `env:"NB_PROXY_READ_TIMEOUT" envDefault:"30s" json:"read_timeout"`
@@ -76,20 +73,7 @@ type Config struct {
EnableGRPC bool `env:"NB_PROXY_ENABLE_GRPC" envDefault:"false" json:"enable_grpc"` EnableGRPC bool `env:"NB_PROXY_ENABLE_GRPC" envDefault:"false" json:"enable_grpc"`
// Reverse Proxy Configuration // Reverse Proxy Configuration
// HTTPListenAddress is the address for HTTP (default ":80") ReverseProxy reverseproxy.Config `json:"reverse_proxy"`
HTTPListenAddress string `json:"http_listen_address"`
// EnableHTTPS enables automatic HTTPS with Let's Encrypt
EnableHTTPS bool `json:"enable_https"`
// TLSEmail is the email for Let's Encrypt registration
TLSEmail string `json:"tls_email"`
// CertCacheDir is the directory to cache certificates (default "./certs")
CertCacheDir string `json:"cert_cache_dir"`
// OIDCConfig is the global OIDC/OAuth configuration for authentication
OIDCConfig *oidc.Config `json:"oidc_config,omitempty"`
} }
// ParseAndLoad parses configuration from environment variables // ParseAndLoad parses configuration from environment variables
@@ -138,11 +122,11 @@ func LoadFromFileOrEnv(configPath string) (Config, error) {
return Config{}, fmt.Errorf("failed to load config from file: %w", err) return Config{}, fmt.Errorf("failed to load config from file: %w", err)
} }
cfg = fileCfg cfg = fileCfg
} } else {
// Parse environment variables (will override file config with any set env vars)
// Parse environment variables (will override file config with any set env vars) if err := env.Parse(&cfg); err != nil {
if err := env.Parse(&cfg); err != nil { return Config{}, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err)
return Config{}, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err) }
} }
if err := cfg.Validate(); err != nil { if err := cfg.Validate(); err != nil {
@@ -228,10 +212,6 @@ func (c *Config) UnmarshalJSON(data []byte) error {
// Validate checks if the configuration is valid // Validate checks if the configuration is valid
func (c *Config) Validate() error { func (c *Config) Validate() error {
if c.ListenAddress == "" {
return errors.New("listen_address is required")
}
validLogLevels := map[string]bool{ validLogLevels := map[string]bool{
"debug": true, "debug": true,
"info": true, "info": true,

View File

@@ -86,33 +86,24 @@ func NewServer(config Config) (*Server, error) {
exposedServices: make(map[string]*ExposedServiceConfig), exposedServices: make(map[string]*ExposedServiceConfig),
} }
// Set defaults for reverse proxy config if not provided // Create reverse proxy using embedded config
httpListenAddr := config.HTTPListenAddress proxy, err := reverseproxy.New(config.ReverseProxy)
if httpListenAddr == "" { if err != nil {
httpListenAddr = ":54321" // Use port 54321 for local testing return nil, fmt.Errorf("failed to create reverse proxy: %w", err)
} }
// Create reverse proxy with request callback // Set request data callback
proxyConfig := reverseproxy.Config{ proxy.SetRequestCallback(func(data reverseproxy.RequestData) {
HTTPListenAddress: httpListenAddr, log.WithFields(log.Fields{
EnableHTTPS: config.EnableHTTPS, "service_id": data.ServiceID,
TLSEmail: config.TLSEmail, "host": data.Host,
CertCacheDir: config.CertCacheDir, "method": data.Method,
RequestDataCallback: func(data reverseproxy.RequestData) { "path": data.Path,
log.WithFields(log.Fields{ "response_code": data.ResponseCode,
"service_id": data.ServiceID, "duration_ms": data.DurationMs,
"host": data.Host, "source_ip": data.SourceIP,
"method": data.Method, }).Info("Access log received")
"path": data.Path, })
"response_code": data.ResponseCode,
"duration_ms": data.DurationMs,
"source_ip": data.SourceIP,
}).Info("Access log received")
},
// Use global OIDC configuration from config
OIDCConfig: config.OIDCConfig,
}
proxy, err := reverseproxy.New(proxyConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create reverse proxy: %w", err) return nil, fmt.Errorf("failed to create reverse proxy: %w", err)
} }
@@ -140,7 +131,7 @@ func (s *Server) Start() error {
s.isRunning = true s.isRunning = true
s.mu.Unlock() s.mu.Unlock()
log.Infof("Starting proxy reverse proxy server on %s", s.config.ListenAddress) log.Infof("Starting proxy reverse proxy server on %s", s.config.ReverseProxy.ListenAddress)
// Start reverse proxy // Start reverse proxy
if err := s.proxy.Start(); err != nil { if err := s.proxy.Start(); err != nil {
@@ -185,9 +176,9 @@ func (s *Server) Start() error {
&reverseproxy.RouteConfig{ &reverseproxy.RouteConfig{
ID: "test", ID: "test",
Domain: "test.netbird.io", Domain: "test.netbird.io",
PathMappings: map[string]string{"/": "localhost:8080"}, PathMappings: map[string]string{"/": "localhost:8181"},
Conn: reverseproxy.NewDefaultConn(),
AuthConfig: testAuthConfig, AuthConfig: testAuthConfig,
SetupKey: "setup-key",
}); err != nil { }); err != nil {
log.Warn("Failed to add test route: ", err) log.Warn("Failed to add test route: ", err)
} }