diff --git a/proxy/cmd/root.go b/proxy/cmd/root.go index 337c15c15..4a02e3003 100644 --- a/proxy/cmd/root.go +++ b/proxy/cmd/root.go @@ -52,7 +52,7 @@ func run(cmd *cobra.Command, args []string) error { log.Infof("Starting Netbird Proxy - %s", version.Short()) log.Debugf("Full version info: %s", version.String()) 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) // Create server instance diff --git a/proxy/config.example.json b/proxy/config.example.json index 744abd534..a6d8e2498 100644 --- a/proxy/config.example.json +++ b/proxy/config.example.json @@ -1,5 +1,4 @@ { - "listen_address": ":443", "read_timeout": "30s", "write_timeout": "30s", "idle_timeout": "60s", @@ -8,20 +7,24 @@ "grpc_listen_address": ":50051", "proxy_id": "proxy-1", "enable_grpc": true, - "http_listen_address": ":80", - "enable_https": false, - "tls_email": "your-email@example.com", - "cert_cache_dir": "./certs", - "oidc_config": { - "provider_url": "https://your-oidc-provider.com", - "client_id": "your-client-id", - "client_secret": "your-client-secret-if-needed", - "redirect_url": "http://localhost:80/auth/callback", - "scopes": ["openid", "profile", "email"], - "jwt_keys_location": "https://your-oidc-provider.com/.well-known/jwks.json", - "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" + "reverse_proxy": { + "listen_address": ":443", + "management_url": "https://api.netbird.io", + "http_listen_address": ":80", + "enable_https": false, + "tls_email": "your-email@example.com", + "cert_cache_dir": "./certs", + "oidc_config": { + "provider_url": "https://your-oidc-provider.com", + "client_id": "your-client-id", + "client_secret": "your-client-secret-if-needed", + "redirect_url": "http://localhost:80/auth/callback", + "scopes": ["openid", "profile", "email"], + "jwt_keys_location": "https://your-oidc-provider.com/.well-known/jwks.json", + "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" + } } } \ No newline at end of file diff --git a/proxy/internal/reverseproxy/config.go b/proxy/internal/reverseproxy/config.go index e07bb4629..9e9b9bc1c 100644 --- a/proxy/internal/reverseproxy/config.go +++ b/proxy/internal/reverseproxy/config.go @@ -1,10 +1,10 @@ package reverseproxy import ( - "net" "net/http" "net/http/httputil" + "github.com/netbirdio/netbird/client/embed" "github.com/netbirdio/netbird/proxy/internal/auth" "github.com/netbirdio/netbird/proxy/internal/auth/oidc" ) @@ -12,28 +12,28 @@ import ( // Config holds the reverse proxy configuration type Config struct { // 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") // 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 bool + EnableHTTPS bool `env:"NB_REVERSE_PROXY_ENABLE_HTTPS" envDefault:"false" json:"enable_https"` // 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 string - - // RequestDataCallback is called for each proxied request with metrics - RequestDataCallback RequestDataCallback + CertCacheDir string `env:"NB_REVERSE_PROXY_CERT_CACHE_DIR" envDefault:"./certs" json:"cert_cache_dir"` // OIDCConfig is the global OIDC/OAuth configuration for authentication // This is shared across all routes that use Bearer authentication // If nil, routes with Bearer auth will fail to initialize - OIDCConfig *oidc.Config + OIDCConfig *oidc.Config `json:"oidc_config"` } // 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. PathMappings map[string]string - // Conn is the network connection to use for this route - // This allows routing through specific tunnels (e.g., WireGuard) per route - // This connection will be reused for all requests to this route - Conn net.Conn + SetupKey string + nbClient *embed.Client // AuthConfig is optional authentication configuration for this route // Configure ONE of: BasicAuth, PIN, or Bearer (JWT/OIDC) diff --git a/proxy/internal/reverseproxy/default_conn.go b/proxy/internal/reverseproxy/default_conn.go deleted file mode 100644 index 803bf9c1b..000000000 --- a/proxy/internal/reverseproxy/default_conn.go +++ /dev/null @@ -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), - } -} diff --git a/proxy/internal/reverseproxy/handler.go b/proxy/internal/reverseproxy/handler.go index 403528ac0..be572365b 100644 --- a/proxy/internal/reverseproxy/handler.go +++ b/proxy/internal/reverseproxy/handler.go @@ -1,8 +1,6 @@ package reverseproxy import ( - "context" - "net" "net/http" "net/http/httputil" "net/url" @@ -187,32 +185,15 @@ func (p *Proxy) createProxy(routeConfig *RouteConfig, target string) *httputil.R // Create reverse proxy proxy := httputil.NewSingleHostReverseProxy(targetURL) - // Check if this is a defaultConn (for testing) - if dc, ok := routeConfig.Conn.(*defaultConn); ok { - // For defaultConn, use its dialer directly - proxy.Transport = &http.Transport{ - DialContext: dc.dialer.DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * 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) + // Configure transport to use the provided connection (WireGuard, etc.) + proxy.Transport = &http.Transport{ + DialContext: routeConfig.nbClient.DialContext, + MaxIdleConns: 1, + MaxIdleConnsPerHost: 1, + IdleConnTimeout: 0, // Keep alive indefinitely + DisableKeepAlives: false, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, } // Custom error handler diff --git a/proxy/internal/reverseproxy/proxy.go b/proxy/internal/reverseproxy/proxy.go index 6a931f867..66f3e7a93 100644 --- a/proxy/internal/reverseproxy/proxy.go +++ b/proxy/internal/reverseproxy/proxy.go @@ -49,10 +49,9 @@ func New(config Config) (*Proxy, error) { } p := &Proxy{ - config: config, - routes: make(map[string]*RouteConfig), - isRunning: false, - requestCallback: config.RequestDataCallback, + config: config, + routes: make(map[string]*RouteConfig), + isRunning: false, } // Initialize OIDC handler if OIDC is configured @@ -65,6 +64,13 @@ func New(config Config) (*Proxy, error) { 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 func (p *Proxy) GetConfig() Config { return p.config diff --git a/proxy/internal/reverseproxy/routes.go b/proxy/internal/reverseproxy/routes.go index c84511131..a09d6ed7a 100644 --- a/proxy/internal/reverseproxy/routes.go +++ b/proxy/internal/reverseproxy/routes.go @@ -1,9 +1,17 @@ package reverseproxy import ( + "context" "fmt" + "time" log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/embed" +) + +const ( + clientStartupTimeout = 30 * time.Second ) // AddRoute adds a new route to the proxy @@ -20,8 +28,8 @@ func (p *Proxy) AddRoute(route *RouteConfig) error { if len(route.PathMappings) == 0 { return fmt.Errorf("route must have at least one path mapping") } - if route.Conn == nil { - return fmt.Errorf("route connection (Conn) is required") + if route.SetupKey == "" { + return fmt.Errorf("route setup key is required") } 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) } + 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 p.routes[route.Domain] = route diff --git a/proxy/internal/reverseproxy/server.go b/proxy/internal/reverseproxy/server.go index a329bfb18..f9389266f 100644 --- a/proxy/internal/reverseproxy/server.go +++ b/proxy/internal/reverseproxy/server.go @@ -9,7 +9,7 @@ import ( "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 { p.mu.Lock() if p.isRunning { @@ -29,7 +29,7 @@ func (p *Proxy) Start() error { 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 { // Setup autocert manager with dynamic host policy 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{ Addr: p.config.ListenAddress, Handler: handler, TLSConfig: p.autocertManager.TLSConfig(), } - log.Infof("Starting HTTPS reverse proxy server on %s", p.config.ListenAddress) - if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { - return fmt.Errorf("HTTPS server failed: %w", err) - } + go func() { + log.Infof("Starting HTTPS reverse proxy server on %s", p.config.ListenAddress) + if err := p.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed { + log.Errorf("HTTPS server failed: %v", err) + } + }() 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 { p.server = &http.Server{ Addr: p.config.HTTPListenAddress, Handler: handler, } - log.Infof("Starting HTTP reverse proxy server on %s (HTTPS disabled)", p.config.HTTPListenAddress) - if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - return fmt.Errorf("HTTP server failed: %w", err) - } + go func() { + log.Infof("Starting HTTP reverse proxy server on %s (HTTPS disabled)", p.config.HTTPListenAddress) + if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Errorf("HTTP server failed: %w", err) + } + }() return nil } diff --git a/proxy/pkg/proxy/config.go b/proxy/pkg/proxy/config.go index d1765c585..6f7c86902 100644 --- a/proxy/pkg/proxy/config.go +++ b/proxy/pkg/proxy/config.go @@ -10,7 +10,7 @@ import ( "github.com/caarlos0/env/v11" - "github.com/netbirdio/netbird/proxy/internal/auth/oidc" + "github.com/netbirdio/netbird/proxy/internal/reverseproxy" ) var ( @@ -48,9 +48,6 @@ func (d Duration) ToDuration() time.Duration { // Config holds the configuration for the reverse proxy server 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 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"` // Reverse Proxy Configuration - // HTTPListenAddress is the address for HTTP (default ":80") - 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"` + ReverseProxy reverseproxy.Config `json:"reverse_proxy"` } // 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) } cfg = fileCfg - } - - // Parse environment variables (will override file config with any set env vars) - if err := env.Parse(&cfg); err != nil { - return Config{}, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err) + } else { + // Parse environment variables (will override file config with any set env vars) + if err := env.Parse(&cfg); err != nil { + return Config{}, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err) + } } if err := cfg.Validate(); err != nil { @@ -228,10 +212,6 @@ func (c *Config) UnmarshalJSON(data []byte) error { // Validate checks if the configuration is valid func (c *Config) Validate() error { - if c.ListenAddress == "" { - return errors.New("listen_address is required") - } - validLogLevels := map[string]bool{ "debug": true, "info": true, diff --git a/proxy/pkg/proxy/server.go b/proxy/pkg/proxy/server.go index 8d94eea0b..32330da78 100644 --- a/proxy/pkg/proxy/server.go +++ b/proxy/pkg/proxy/server.go @@ -86,33 +86,24 @@ func NewServer(config Config) (*Server, error) { exposedServices: make(map[string]*ExposedServiceConfig), } - // Set defaults for reverse proxy config if not provided - httpListenAddr := config.HTTPListenAddress - if httpListenAddr == "" { - httpListenAddr = ":54321" // Use port 54321 for local testing + // Create reverse proxy using embedded config + proxy, err := reverseproxy.New(config.ReverseProxy) + if err != nil { + return nil, fmt.Errorf("failed to create reverse proxy: %w", err) } - // Create reverse proxy with request callback - proxyConfig := reverseproxy.Config{ - HTTPListenAddress: httpListenAddr, - EnableHTTPS: config.EnableHTTPS, - TLSEmail: config.TLSEmail, - CertCacheDir: config.CertCacheDir, - RequestDataCallback: func(data reverseproxy.RequestData) { - log.WithFields(log.Fields{ - "service_id": data.ServiceID, - "host": data.Host, - "method": data.Method, - "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) + // Set request data callback + proxy.SetRequestCallback(func(data reverseproxy.RequestData) { + log.WithFields(log.Fields{ + "service_id": data.ServiceID, + "host": data.Host, + "method": data.Method, + "path": data.Path, + "response_code": data.ResponseCode, + "duration_ms": data.DurationMs, + "source_ip": data.SourceIP, + }).Info("Access log received") + }) if err != nil { return nil, fmt.Errorf("failed to create reverse proxy: %w", err) } @@ -140,7 +131,7 @@ func (s *Server) Start() error { s.isRunning = true 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 if err := s.proxy.Start(); err != nil { @@ -185,9 +176,9 @@ func (s *Server) Start() error { &reverseproxy.RouteConfig{ ID: "test", Domain: "test.netbird.io", - PathMappings: map[string]string{"/": "localhost:8080"}, - Conn: reverseproxy.NewDefaultConn(), + PathMappings: map[string]string{"/": "localhost:8181"}, AuthConfig: testAuthConfig, + SetupKey: "setup-key", }); err != nil { log.Warn("Failed to add test route: ", err) }