This commit is contained in:
pascal
2026-01-16 12:01:52 +01:00
parent 3b832d1f21
commit 183619d1e1
20 changed files with 34 additions and 525 deletions

View File

@@ -1,73 +0,0 @@
package errors
import "fmt"
// Configuration errors
func NewConfigInvalid(message string) *AppError {
return New(CodeConfigInvalid, message)
}
func NewConfigNotFound(path string) *AppError {
return New(CodeConfigNotFound, fmt.Sprintf("configuration file not found: %s", path))
}
func WrapConfigParseFailed(err error, path string) *AppError {
return Wrap(CodeConfigParseFailed, fmt.Sprintf("failed to parse configuration file: %s", path), err)
}
// Server errors
func NewServerStartFailed(err error, reason string) *AppError {
return Wrap(CodeServerStartFailed, fmt.Sprintf("server start failed: %s", reason), err)
}
func NewServerStopFailed(err error) *AppError {
return Wrap(CodeServerStopFailed, "server shutdown failed", err)
}
func NewServerAlreadyRunning() *AppError {
return New(CodeServerAlreadyRunning, "server is already running")
}
func NewServerNotRunning() *AppError {
return New(CodeServerNotRunning, "server is not running")
}
// Proxy errors
func NewProxyBackendUnavailable(backend string, err error) *AppError {
return Wrap(CodeProxyBackendUnavailable, fmt.Sprintf("backend unavailable: %s", backend), err)
}
func NewProxyTimeout(backend string) *AppError {
return New(CodeProxyTimeout, fmt.Sprintf("request to backend timed out: %s", backend))
}
func NewProxyInvalidTarget(target string, err error) *AppError {
return Wrap(CodeProxyInvalidTarget, fmt.Sprintf("invalid proxy target: %s", target), err)
}
// Network errors
func NewNetworkTimeout(operation string) *AppError {
return New(CodeNetworkTimeout, fmt.Sprintf("network timeout: %s", operation))
}
func NewNetworkUnreachable(host string) *AppError {
return New(CodeNetworkUnreachable, fmt.Sprintf("network unreachable: %s", host))
}
func NewNetworkRefused(host string) *AppError {
return New(CodeNetworkRefused, fmt.Sprintf("connection refused: %s", host))
}
// Internal errors
func NewInternalError(message string) *AppError {
return New(CodeInternalError, message)
}
func WrapInternalError(err error, message string) *AppError {
return Wrap(CodeInternalError, message, err)
}

View File

@@ -1,138 +0,0 @@
package errors
import (
"errors"
"fmt"
)
// Error codes for categorizing errors
type Code string
const (
// Configuration errors
CodeConfigInvalid Code = "CONFIG_INVALID"
CodeConfigNotFound Code = "CONFIG_NOT_FOUND"
CodeConfigParseFailed Code = "CONFIG_PARSE_FAILED"
// Server errors
CodeServerStartFailed Code = "SERVER_START_FAILED"
CodeServerStopFailed Code = "SERVER_STOP_FAILED"
CodeServerAlreadyRunning Code = "SERVER_ALREADY_RUNNING"
CodeServerNotRunning Code = "SERVER_NOT_RUNNING"
// Proxy errors
CodeProxyBackendUnavailable Code = "PROXY_BACKEND_UNAVAILABLE"
CodeProxyTimeout Code = "PROXY_TIMEOUT"
CodeProxyInvalidTarget Code = "PROXY_INVALID_TARGET"
// Network errors
CodeNetworkTimeout Code = "NETWORK_TIMEOUT"
CodeNetworkUnreachable Code = "NETWORK_UNREACHABLE"
CodeNetworkRefused Code = "NETWORK_REFUSED"
// Internal errors
CodeInternalError Code = "INTERNAL_ERROR"
CodeUnknownError Code = "UNKNOWN_ERROR"
)
// AppError represents a structured application error
type AppError struct {
Code Code // Error code for categorization
Message string // Human-readable error message
Cause error // Underlying error (if any)
}
// Error implements the error interface
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
// Unwrap returns the underlying error (for errors.Is and errors.As)
func (e *AppError) Unwrap() error {
return e.Cause
}
// Is checks if the error matches the target
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// New creates a new AppError
func New(code Code, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
// Wrap wraps an existing error with additional context
func Wrap(code Code, message string, cause error) *AppError {
return &AppError{
Code: code,
Message: message,
Cause: cause,
}
}
// Wrapf wraps an error with a formatted message
func Wrapf(code Code, cause error, format string, args ...interface{}) *AppError {
return &AppError{
Code: code,
Message: fmt.Sprintf(format, args...),
Cause: cause,
}
}
// GetCode extracts the error code from an error
func GetCode(err error) Code {
var appErr *AppError
if errors.As(err, &appErr) {
return appErr.Code
}
return CodeUnknownError
}
// HasCode checks if an error has a specific code
func HasCode(err error, code Code) bool {
return GetCode(err) == code
}
// IsConfigError checks if an error is configuration-related
func IsConfigError(err error) bool {
code := GetCode(err)
return code == CodeConfigInvalid ||
code == CodeConfigNotFound ||
code == CodeConfigParseFailed
}
// IsServerError checks if an error is server-related
func IsServerError(err error) bool {
code := GetCode(err)
return code == CodeServerStartFailed ||
code == CodeServerStopFailed ||
code == CodeServerAlreadyRunning ||
code == CodeServerNotRunning
}
// IsProxyError checks if an error is proxy-related
func IsProxyError(err error) bool {
code := GetCode(err)
return code == CodeProxyBackendUnavailable ||
code == CodeProxyTimeout ||
code == CodeProxyInvalidTarget
}
// IsNetworkError checks if an error is network-related
func IsNetworkError(err error) bool {
code := GetCode(err)
return code == CodeNetworkTimeout ||
code == CodeNetworkUnreachable ||
code == CodeNetworkRefused
}

View File

@@ -1,160 +0,0 @@
package errors
import (
"errors"
"testing"
)
func TestAppError_Error(t *testing.T) {
tests := []struct {
name string
err *AppError
expected string
}{
{
name: "error without cause",
err: New(CodeConfigInvalid, "invalid configuration"),
expected: "[CONFIG_INVALID] invalid configuration",
},
{
name: "error with cause",
err: Wrap(CodeServerStartFailed, "failed to bind port", errors.New("address already in use")),
expected: "[SERVER_START_FAILED] failed to bind port: address already in use",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.expected {
t.Errorf("Error() = %v, want %v", got, tt.expected)
}
})
}
}
func TestGetCode(t *testing.T) {
tests := []struct {
name string
err error
expected Code
}{
{
name: "app error",
err: New(CodeConfigInvalid, "test"),
expected: CodeConfigInvalid,
},
{
name: "wrapped app error",
err: Wrap(CodeServerStartFailed, "test", errors.New("cause")),
expected: CodeServerStartFailed,
},
{
name: "standard error",
err: errors.New("standard error"),
expected: CodeUnknownError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetCode(tt.err); got != tt.expected {
t.Errorf("GetCode() = %v, want %v", got, tt.expected)
}
})
}
}
func TestHasCode(t *testing.T) {
err := New(CodeConfigInvalid, "invalid config")
if !HasCode(err, CodeConfigInvalid) {
t.Error("HasCode() should return true for matching code")
}
if HasCode(err, CodeServerStartFailed) {
t.Error("HasCode() should return false for non-matching code")
}
}
func TestIsConfigError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "config invalid error",
err: New(CodeConfigInvalid, "test"),
expected: true,
},
{
name: "config not found error",
err: New(CodeConfigNotFound, "test"),
expected: true,
},
{
name: "server error",
err: New(CodeServerStartFailed, "test"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsConfigError(tt.err); got != tt.expected {
t.Errorf("IsConfigError() = %v, want %v", got, tt.expected)
}
})
}
}
func TestErrorUnwrap(t *testing.T) {
cause := errors.New("root cause")
err := Wrap(CodeInternalError, "wrapped error", cause)
unwrapped := errors.Unwrap(err)
if unwrapped != cause {
t.Errorf("Unwrap() = %v, want %v", unwrapped, cause)
}
}
func TestErrorIs(t *testing.T) {
err1 := New(CodeConfigInvalid, "test1")
err2 := New(CodeConfigInvalid, "test2")
err3 := New(CodeServerStartFailed, "test3")
if !errors.Is(err1, err2) {
t.Error("errors.Is() should return true for same error code")
}
if errors.Is(err1, err3) {
t.Error("errors.Is() should return false for different error codes")
}
}
func TestCommonConstructors(t *testing.T) {
t.Run("NewConfigNotFound", func(t *testing.T) {
err := NewConfigNotFound("/path/to/config")
if GetCode(err) != CodeConfigNotFound {
t.Error("NewConfigNotFound should create CONFIG_NOT_FOUND error")
}
})
t.Run("NewServerAlreadyRunning", func(t *testing.T) {
err := NewServerAlreadyRunning()
if GetCode(err) != CodeServerAlreadyRunning {
t.Error("NewServerAlreadyRunning should create SERVER_ALREADY_RUNNING error")
}
})
t.Run("NewProxyBackendUnavailable", func(t *testing.T) {
cause := errors.New("connection refused")
err := NewProxyBackendUnavailable("http://backend", cause)
if GetCode(err) != CodeProxyBackendUnavailable {
t.Error("NewProxyBackendUnavailable should create PROXY_BACKEND_UNAVAILABLE error")
}
if !errors.Is(err.Unwrap(), cause) {
t.Error("NewProxyBackendUnavailable should wrap the cause")
}
})
}

View File

@@ -77,7 +77,6 @@ func (s *Server) Start() error {
return fmt.Errorf("failed to listen: %w", err)
}
// Configure gRPC server with keepalive
s.grpcServer = grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
Time: 30 * time.Second,
@@ -114,7 +113,6 @@ func (s *Server) Stop(ctx context.Context) error {
log.Info("Stopping gRPC server...")
// Cancel all active streams
s.mu.Lock()
for _, streamCtx := range s.streams {
streamCtx.cancel()
@@ -123,7 +121,6 @@ func (s *Server) Stop(ctx context.Context) error {
s.streams = make(map[string]*StreamContext)
s.mu.Unlock()
// Graceful stop with timeout
stopped := make(chan struct{})
go func() {
s.grpcServer.GracefulStop()
@@ -154,7 +151,6 @@ func (s *Server) Stream(stream pb.ProxyService_StreamServer) error {
controlID := fmt.Sprintf("control-%d", time.Now().Unix())
// Create stream context
streamCtx := &StreamContext{
stream: stream,
sendChan: make(chan *pb.ProxyMessage, 100),
@@ -163,22 +159,18 @@ func (s *Server) Stream(stream pb.ProxyService_StreamServer) error {
controlID: controlID,
}
// Register stream
s.mu.Lock()
s.streams[controlID] = streamCtx
s.mu.Unlock()
log.Infof("Control service connected: %s", controlID)
// Start goroutine to send ProxyMessages to control service
sendDone := make(chan error, 1)
go s.sendLoop(streamCtx, sendDone)
// Start goroutine to receive ControlMessages from control service
recvDone := make(chan error, 1)
go s.receiveLoop(streamCtx, recvDone)
// Wait for either send or receive to complete
select {
case err := <-sendDone:
log.Infof("Control service %s send loop ended: %v", controlID, err)
@@ -202,7 +194,6 @@ func (s *Server) sendLoop(streamCtx *StreamContext, done chan<- error) {
return
}
// Send ProxyMessage to control service
if err := streamCtx.stream.Send(msg); err != nil {
log.Errorf("Failed to send message to control service: %v", err)
done <- err
@@ -219,7 +210,6 @@ func (s *Server) sendLoop(streamCtx *StreamContext, done chan<- error) {
// receiveLoop handles receiving ControlMessages from the control service
func (s *Server) receiveLoop(streamCtx *StreamContext, done chan<- error) {
for {
// Receive ControlMessage from control service (client)
controlMsg, err := streamCtx.stream.Recv()
if err != nil {
log.Debugf("Stream receive error: %v", err)
@@ -227,7 +217,6 @@ func (s *Server) receiveLoop(streamCtx *StreamContext, done chan<- error) {
return
}
// Handle different ControlMessage types
switch m := controlMsg.Message.(type) {
case *pb.ControlMessage_Event:
if s.handler != nil {
@@ -271,7 +260,6 @@ func (s *Server) SendProxyMessage(msg *pb.ProxyMessage) {
for _, streamCtx := range s.streams {
select {
case streamCtx.sendChan <- msg:
// Message queued successfully
default:
log.Warn("Send channel full, dropping message")
}

View File

@@ -63,7 +63,7 @@ type Config struct {
// LogLevel sets the logging verbosity (debug, info, warn, error)
LogLevel string `env:"NB_PROXY_LOG_LEVEL" envDefault:"info" json:"log_level"`
// GRPCListenAddress is the address for the gRPC control server (empty to disable)
// GRPCListenAddress is the address for the gRPC control server
GRPCListenAddress string `env:"NB_PROXY_GRPC_LISTEN_ADDRESS" envDefault:":50051" json:"grpc_listen_address"`
// ProxyID is a unique identifier for this proxy instance
@@ -111,7 +111,6 @@ func LoadFromFile(path string) (Config, error) {
}
// LoadFromFileOrEnv loads configuration from a file if path is provided, otherwise from environment variables
// Environment variables will override file-based configuration if both are present
func LoadFromFileOrEnv(configPath string) (Config, error) {
var cfg Config
@@ -123,7 +122,6 @@ func LoadFromFileOrEnv(configPath string) (Config, error) {
}
cfg = fileCfg
} 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)
}
@@ -137,30 +135,24 @@ func LoadFromFileOrEnv(configPath string) (Config, error) {
}
// 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 {
@@ -171,15 +163,12 @@ func (c *Config) UnmarshalJSON(data []byte) error {
}
}
// 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 {
@@ -190,13 +179,11 @@ func (c *Config) UnmarshalJSON(data []byte) error {
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 {

View File

@@ -95,13 +95,16 @@ func NewServer(config Config) (*Server, error) {
// 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,
"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,
"auth_mechanism": data.AuthMechanism,
"user_id": data.UserID,
"auth_success": data.AuthSuccess,
}).Info("Access log received")
})
if err != nil {
@@ -176,9 +179,9 @@ func (s *Server) Start() error {
&reverseproxy.RouteConfig{
ID: "test",
Domain: "test.netbird.io",
PathMappings: map[string]string{"/": "localhost:8181"},
PathMappings: map[string]string{"/": "100.116.118.156:8181"},
AuthConfig: testAuthConfig,
SetupKey: "setup-key",
SetupKey: "88B2382A-93D2-47A9-A80F-D0055D741636",
}); err != nil {
log.Warn("Failed to add test route: ", err)
}