mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-20 17:26:40 +00:00
using go http reverseproxy with OIDC auth
This commit is contained in:
@@ -5,15 +5,47 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/internal/reverseproxy"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedToParseConfig = errors.New("failed to parse config from env")
|
||||
)
|
||||
|
||||
// Duration is a time.Duration that can be unmarshaled from JSON as a string
|
||||
type Duration time.Duration
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler for Duration
|
||||
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsed, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = Duration(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler for Duration
|
||||
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Duration(d).String())
|
||||
}
|
||||
|
||||
// ToDuration converts Duration to time.Duration
|
||||
func (d Duration) ToDuration() time.Duration {
|
||||
return time.Duration(d)
|
||||
}
|
||||
|
||||
// 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")
|
||||
@@ -42,6 +74,22 @@ type Config struct {
|
||||
|
||||
// EnableGRPC enables the gRPC control server
|
||||
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 *reverseproxy.OIDCConfig `json:"oidc_config,omitempty"`
|
||||
}
|
||||
|
||||
// ParseAndLoad parses configuration from environment variables
|
||||
@@ -104,6 +152,80 @@ func LoadFromFileOrEnv(configPath string) (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements custom JSON unmarshaling with automatic duration parsing
|
||||
// Uses reflection to find all time.Duration fields and parse them from string
|
||||
func (c *Config) UnmarshalJSON(data []byte) error {
|
||||
// First unmarshal into a map to get raw values
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get reflection value and type
|
||||
val := reflect.ValueOf(c).Elem()
|
||||
typ := val.Type()
|
||||
|
||||
// Iterate through all fields
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// Get JSON tag name
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
if jsonTag == "" || jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse tag to get field name (handle omitempty, etc.)
|
||||
jsonFieldName := jsonTag
|
||||
if idx := len(jsonTag); idx > 0 {
|
||||
for j, c := range jsonTag {
|
||||
if c == ',' {
|
||||
jsonFieldName = jsonTag[:j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get raw value from JSON
|
||||
rawValue, exists := raw[jsonFieldName]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this field is a time.Duration
|
||||
if field.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||
// Try to parse as string duration
|
||||
if strValue, ok := rawValue.(string); ok {
|
||||
duration, err := time.ParseDuration(strValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid duration for field %s: %w", jsonFieldName, err)
|
||||
}
|
||||
field.Set(reflect.ValueOf(duration))
|
||||
} else {
|
||||
return fmt.Errorf("field %s must be a duration string", jsonFieldName)
|
||||
}
|
||||
} else {
|
||||
// For non-duration fields, unmarshal normally
|
||||
fieldData, err := json.Marshal(rawValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal field %s: %w", jsonFieldName, err)
|
||||
}
|
||||
|
||||
// Create a new instance of the field type
|
||||
if field.CanSet() {
|
||||
newVal := reflect.New(field.Type())
|
||||
if err := json.Unmarshal(fieldData, newVal.Interface()); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal field %s: %w", jsonFieldName, err)
|
||||
}
|
||||
field.Set(newVal.Elem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid
|
||||
func (c *Config) Validate() error {
|
||||
if c.ListenAddress == "" {
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
type Server struct {
|
||||
config Config
|
||||
grpcServer *grpcpkg.Server
|
||||
caddyProxy *reverseproxy.CaddyProxy
|
||||
proxy *reverseproxy.Proxy
|
||||
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
@@ -84,30 +84,37 @@ func NewServer(config Config) (*Server, error) {
|
||||
exposedServices: make(map[string]*ExposedServiceConfig),
|
||||
}
|
||||
|
||||
// Create Caddy reverse proxy with request callback
|
||||
caddyConfig := reverseproxy.Config{
|
||||
ListenAddress: ":54321", // Use port 54321 for local testing
|
||||
EnableHTTPS: false, // TODO: Add HTTPS support
|
||||
RequestDataCallback: func(data *reverseproxy.RequestData) {
|
||||
// This is where access log data arrives - SET BREAKPOINT HERE
|
||||
// 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 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")
|
||||
|
||||
// TODO: Send via gRPC to control service
|
||||
// This would send pb.ProxyRequestData via the gRPC stream
|
||||
},
|
||||
// Use global OIDC configuration from config
|
||||
OIDCConfig: config.OIDCConfig,
|
||||
}
|
||||
caddyProxy, err := reverseproxy.New(caddyConfig)
|
||||
proxy, err := reverseproxy.New(proxyConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Caddy proxy: %w", err)
|
||||
return nil, fmt.Errorf("failed to create reverse proxy: %w", err)
|
||||
}
|
||||
server.caddyProxy = caddyProxy
|
||||
server.proxy = proxy
|
||||
|
||||
// Create gRPC server if enabled
|
||||
if config.EnableGRPC && config.GRPCListenAddress != "" {
|
||||
@@ -131,14 +138,14 @@ func (s *Server) Start() error {
|
||||
s.isRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Infof("Starting Caddy reverse proxy server on %s", s.config.ListenAddress)
|
||||
log.Infof("Starting proxy reverse proxy server on %s", s.config.ListenAddress)
|
||||
|
||||
// Start Caddy proxy
|
||||
if err := s.caddyProxy.Start(); err != nil {
|
||||
// Start reverse proxy
|
||||
if err := s.proxy.Start(); err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to start Caddy proxy: %w", err)
|
||||
return fmt.Errorf("failed to start reverse proxy: %w", err)
|
||||
}
|
||||
|
||||
// Start gRPC server if configured
|
||||
@@ -162,21 +169,32 @@ func (s *Server) Start() error {
|
||||
s.sendProxyEvent(pb.ProxyEvent_STARTED, "Proxy server started")
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.AddRoute(
|
||||
// Enable Bearer authentication for the test route
|
||||
// OIDC configuration is set globally in the proxy config above
|
||||
testAuthConfig := &reverseproxy.AuthConfig{
|
||||
Bearer: &reverseproxy.BearerConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Register main protected route with auth
|
||||
// The /auth/callback endpoint is automatically handled globally for all routes
|
||||
if err := s.proxy.AddRoute(
|
||||
&reverseproxy.RouteConfig{
|
||||
ID: "test",
|
||||
Domain: "test.netbird.io",
|
||||
PathMappings: map[string]string{"/": "localhost:8080"},
|
||||
Conn: reverseproxy.NewDefaultConn(),
|
||||
AuthConfig: testAuthConfig,
|
||||
}); err != nil {
|
||||
log.Warn("Failed to add test route: ", err)
|
||||
}
|
||||
|
||||
// Block forever - Caddy runs in background
|
||||
<-s.shutdownCtx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down both Caddy and gRPC servers
|
||||
// Stop gracefully shuts down both proxy and gRPC servers
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
if !s.isRunning {
|
||||
@@ -212,9 +230,9 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Shutdown Caddy proxy
|
||||
if err := s.caddyProxy.Stop(ctx); err != nil {
|
||||
caddyErr = fmt.Errorf("Caddy proxy shutdown failed: %w", err)
|
||||
// Shutdown reverse proxy
|
||||
if err := s.proxy.Stop(ctx); err != nil {
|
||||
caddyErr = fmt.Errorf("reverse proxy shutdown failed: %w", err)
|
||||
log.Error(caddyErr)
|
||||
}
|
||||
|
||||
@@ -401,14 +419,14 @@ func (s *Server) handleExposedServiceCreated(serviceID string, peerConfig *PeerC
|
||||
pathMappings[path] = target
|
||||
}
|
||||
|
||||
// Add route to Caddy
|
||||
// Add route to proxy
|
||||
route := &reverseproxy.RouteConfig{
|
||||
ID: serviceID,
|
||||
Domain: upstreamConfig.Domain,
|
||||
PathMappings: pathMappings,
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.AddRoute(route); err != nil {
|
||||
if err := s.proxy.AddRoute(route); err != nil {
|
||||
return fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
|
||||
@@ -449,14 +467,14 @@ func (s *Server) handleExposedServiceUpdated(serviceID string, peerConfig *PeerC
|
||||
pathMappings[path] = target
|
||||
}
|
||||
|
||||
// Update route in Caddy
|
||||
// Update route in proxy
|
||||
route := &reverseproxy.RouteConfig{
|
||||
ID: serviceID,
|
||||
Domain: upstreamConfig.Domain,
|
||||
PathMappings: pathMappings,
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.UpdateRoute(route); err != nil {
|
||||
if err := s.proxy.UpdateRoute(route); err != nil {
|
||||
return fmt.Errorf("failed to update route: %w", err)
|
||||
}
|
||||
|
||||
@@ -485,8 +503,8 @@ func (s *Server) handleExposedServiceRemoved(serviceID string) error {
|
||||
"service_id": serviceID,
|
||||
}).Info("Removing exposed service")
|
||||
|
||||
// Remove route from Caddy
|
||||
if err := s.caddyProxy.RemoveRoute(serviceID); err != nil {
|
||||
// Remove route from proxy
|
||||
if err := s.proxy.RemoveRoute(serviceID); err != nil {
|
||||
return fmt.Errorf("failed to remove route: %w", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user