mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-23 15:19:08 +02:00
trying embedded caddy reverse proxy
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package proxy;
|
||||
|
||||
option go_package = "github.com/netbirdio/netbird/proxy/pkg/grpc/proto";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// ProxyService defines the bidirectional streaming service
|
||||
// The proxy runs this service, control service connects as client
|
||||
service ProxyService {
|
||||
// Stream establishes a bidirectional stream between proxy and control service
|
||||
// Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage
|
||||
rpc Stream(stream ControlMessage) returns (stream ProxyMessage);
|
||||
}
|
||||
|
||||
// ProxyMessage represents messages sent from proxy to control service
|
||||
message ProxyMessage {
|
||||
oneof message {
|
||||
ProxyStats stats = 1;
|
||||
ProxyEvent event = 2;
|
||||
ProxyLog log = 3;
|
||||
ProxyHeartbeat heartbeat = 4;
|
||||
ProxyRequestData request_data = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// ControlMessage represents messages sent from control service to proxy
|
||||
message ControlMessage {
|
||||
oneof message {
|
||||
ControlEvent event = 1;
|
||||
ControlCommand command = 2;
|
||||
ControlConfig config = 3;
|
||||
ExposedServiceEvent exposed_service = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyStats contains proxy statistics
|
||||
message ProxyStats {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
uint64 total_requests = 2;
|
||||
uint64 active_connections = 3;
|
||||
uint64 bytes_sent = 4;
|
||||
uint64 bytes_received = 5;
|
||||
double cpu_usage = 6;
|
||||
double memory_usage_mb = 7;
|
||||
map<string, uint64> status_code_counts = 8;
|
||||
}
|
||||
|
||||
// ProxyEvent represents events from the proxy
|
||||
message ProxyEvent {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
EventType type = 2;
|
||||
string message = 3;
|
||||
map<string, string> metadata = 4;
|
||||
|
||||
enum EventType {
|
||||
UNKNOWN = 0;
|
||||
STARTED = 1;
|
||||
STOPPED = 2;
|
||||
ERROR = 3;
|
||||
BACKEND_UNAVAILABLE = 4;
|
||||
BACKEND_RECOVERED = 5;
|
||||
CONFIG_UPDATED = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyLog represents log entries
|
||||
message ProxyLog {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
LogLevel level = 2;
|
||||
string message = 3;
|
||||
map<string, string> fields = 4;
|
||||
|
||||
enum LogLevel {
|
||||
DEBUG = 0;
|
||||
INFO = 1;
|
||||
WARN = 2;
|
||||
ERROR = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// ProxyHeartbeat is sent periodically to keep connection alive
|
||||
message ProxyHeartbeat {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
string proxy_id = 2;
|
||||
}
|
||||
|
||||
// ControlEvent represents events from control service
|
||||
message ControlEvent {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
string event_id = 2;
|
||||
string message = 3;
|
||||
}
|
||||
|
||||
// ControlCommand represents commands sent to proxy
|
||||
message ControlCommand {
|
||||
string command_id = 1;
|
||||
CommandType type = 2;
|
||||
map<string, string> parameters = 3;
|
||||
|
||||
enum CommandType {
|
||||
UNKNOWN = 0;
|
||||
RELOAD_CONFIG = 1;
|
||||
ENABLE_DEBUG = 2;
|
||||
DISABLE_DEBUG = 3;
|
||||
GET_STATS = 4;
|
||||
SHUTDOWN = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// ControlConfig contains configuration updates from control service
|
||||
message ControlConfig {
|
||||
string config_version = 1;
|
||||
map<string, string> settings = 2;
|
||||
}
|
||||
|
||||
// ExposedServiceEvent represents exposed service lifecycle events
|
||||
message ExposedServiceEvent {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
EventType type = 2;
|
||||
string service_id = 3;
|
||||
PeerConfig peer_config = 4;
|
||||
UpstreamConfig upstream_config = 5;
|
||||
|
||||
enum EventType {
|
||||
UNKNOWN = 0;
|
||||
CREATED = 1;
|
||||
UPDATED = 2;
|
||||
REMOVED = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// PeerConfig contains WireGuard peer configuration
|
||||
message PeerConfig {
|
||||
string peer_id = 1;
|
||||
string public_key = 2;
|
||||
repeated string allowed_ips = 3;
|
||||
string endpoint = 4;
|
||||
string tunnel_ip = 5;
|
||||
uint32 persistent_keepalive = 6;
|
||||
}
|
||||
|
||||
// UpstreamConfig contains reverse proxy upstream configuration
|
||||
message UpstreamConfig {
|
||||
string domain = 1;
|
||||
map<string, string> path_mappings = 2; // path -> port
|
||||
}
|
||||
|
||||
// ProxyRequestData contains metadata about requests routed through the reverse proxy
|
||||
message ProxyRequestData {
|
||||
google.protobuf.Timestamp timestamp = 1;
|
||||
string service_id = 2;
|
||||
string path = 3;
|
||||
int64 duration_ms = 4;
|
||||
string method = 5; // HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
int32 response_code = 6;
|
||||
string source_ip = 7;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// ProxyServiceClient is the client API for ProxyService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ProxyServiceClient interface {
|
||||
// Stream establishes a bidirectional stream between proxy and control service
|
||||
// Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage
|
||||
Stream(ctx context.Context, opts ...grpc.CallOption) (ProxyService_StreamClient, error)
|
||||
}
|
||||
|
||||
type proxyServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewProxyServiceClient(cc grpc.ClientConnInterface) ProxyServiceClient {
|
||||
return &proxyServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *proxyServiceClient) Stream(ctx context.Context, opts ...grpc.CallOption) (ProxyService_StreamClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &ProxyService_ServiceDesc.Streams[0], "/proxy.ProxyService/Stream", opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &proxyServiceStreamClient{stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
type ProxyService_StreamClient interface {
|
||||
Send(*ControlMessage) error
|
||||
Recv() (*ProxyMessage, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
type proxyServiceStreamClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamClient) Send(m *ControlMessage) error {
|
||||
return x.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamClient) Recv() (*ProxyMessage, error) {
|
||||
m := new(ProxyMessage)
|
||||
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ProxyServiceServer is the server API for ProxyService service.
|
||||
// All implementations must embed UnimplementedProxyServiceServer
|
||||
// for forward compatibility
|
||||
type ProxyServiceServer interface {
|
||||
// Stream establishes a bidirectional stream between proxy and control service
|
||||
// Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage
|
||||
Stream(ProxyService_StreamServer) error
|
||||
mustEmbedUnimplementedProxyServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedProxyServiceServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedProxyServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedProxyServiceServer) Stream(ProxyService_StreamServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method Stream not implemented")
|
||||
}
|
||||
func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {}
|
||||
|
||||
// UnsafeProxyServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ProxyServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeProxyServiceServer interface {
|
||||
mustEmbedUnimplementedProxyServiceServer()
|
||||
}
|
||||
|
||||
func RegisterProxyServiceServer(s grpc.ServiceRegistrar, srv ProxyServiceServer) {
|
||||
s.RegisterService(&ProxyService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ProxyService_Stream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(ProxyServiceServer).Stream(&proxyServiceStreamServer{stream})
|
||||
}
|
||||
|
||||
type ProxyService_StreamServer interface {
|
||||
Send(*ProxyMessage) error
|
||||
Recv() (*ControlMessage, error)
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
type proxyServiceStreamServer struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamServer) Send(m *ProxyMessage) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *proxyServiceStreamServer) Recv() (*ControlMessage, error) {
|
||||
m := new(ControlMessage)
|
||||
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ProxyService_ServiceDesc is the grpc.ServiceDesc for ProxyService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ProxyService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "proxy.ProxyService",
|
||||
HandlerType: (*ProxyServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "Stream",
|
||||
Handler: _ProxyService_Stream_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "pkg/grpc/proto/proxy.proto",
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
|
||||
pb "github.com/netbirdio/netbird/proxy/pkg/grpc/proto"
|
||||
)
|
||||
|
||||
// StreamHandler handles incoming messages from control service
|
||||
type StreamHandler interface {
|
||||
HandleControlEvent(ctx context.Context, event *pb.ControlEvent) error
|
||||
HandleControlCommand(ctx context.Context, command *pb.ControlCommand) error
|
||||
HandleControlConfig(ctx context.Context, config *pb.ControlConfig) error
|
||||
HandleExposedServiceEvent(ctx context.Context, event *pb.ExposedServiceEvent) error
|
||||
}
|
||||
|
||||
// Server represents the gRPC server running on the proxy
|
||||
type Server struct {
|
||||
pb.UnimplementedProxyServiceServer
|
||||
|
||||
listenAddr string
|
||||
grpcServer *grpc.Server
|
||||
handler StreamHandler
|
||||
|
||||
mu sync.RWMutex
|
||||
streams map[string]*StreamContext
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// StreamContext holds the context for each active stream
|
||||
type StreamContext struct {
|
||||
stream pb.ProxyService_StreamServer
|
||||
sendChan chan *pb.ProxyMessage
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
controlID string // ID of the connected control service
|
||||
}
|
||||
|
||||
// Config holds gRPC server configuration
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
Handler StreamHandler
|
||||
}
|
||||
|
||||
// NewServer creates a new gRPC server
|
||||
func NewServer(config Config) *Server {
|
||||
return &Server{
|
||||
listenAddr: config.ListenAddr,
|
||||
handler: config.Handler,
|
||||
streams: make(map[string]*StreamContext),
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the gRPC server
|
||||
func (s *Server) Start() error {
|
||||
s.mu.Lock()
|
||||
if s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("gRPC server already running")
|
||||
}
|
||||
s.isRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
lis, err := net.Listen("tcp", s.listenAddr)
|
||||
if err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
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,
|
||||
Timeout: 10 * time.Second,
|
||||
}),
|
||||
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||
MinTime: 10 * time.Second,
|
||||
PermitWithoutStream: true,
|
||||
}),
|
||||
)
|
||||
|
||||
pb.RegisterProxyServiceServer(s.grpcServer, s)
|
||||
|
||||
log.Infof("gRPC server listening on %s", s.listenAddr)
|
||||
|
||||
if err := s.grpcServer.Serve(lis); err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to serve: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the gRPC server
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
if !s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("gRPC server not running")
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Info("Stopping gRPC server...")
|
||||
|
||||
// Cancel all active streams
|
||||
s.mu.Lock()
|
||||
for _, streamCtx := range s.streams {
|
||||
streamCtx.cancel()
|
||||
close(streamCtx.sendChan)
|
||||
}
|
||||
s.streams = make(map[string]*StreamContext)
|
||||
s.mu.Unlock()
|
||||
|
||||
// Graceful stop with timeout
|
||||
stopped := make(chan struct{})
|
||||
go func() {
|
||||
s.grpcServer.GracefulStop()
|
||||
close(stopped)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stopped:
|
||||
log.Info("gRPC server stopped gracefully")
|
||||
case <-ctx.Done():
|
||||
log.Warn("gRPC server graceful stop timeout, forcing stop")
|
||||
s.grpcServer.Stop()
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stream implements the bidirectional streaming RPC
|
||||
// The control service connects as client, proxy is server
|
||||
// Control service sends ControlMessage, Proxy sends ProxyMessage
|
||||
func (s *Server) Stream(stream pb.ProxyService_StreamServer) error {
|
||||
ctx, cancel := context.WithCancel(stream.Context())
|
||||
defer cancel()
|
||||
|
||||
controlID := fmt.Sprintf("control-%d", time.Now().Unix())
|
||||
|
||||
// Create stream context
|
||||
streamCtx := &StreamContext{
|
||||
stream: stream,
|
||||
sendChan: make(chan *pb.ProxyMessage, 100),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
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)
|
||||
return err
|
||||
case err := <-recvDone:
|
||||
log.Infof("Control service %s receive loop ended: %v", controlID, err)
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
log.Infof("Control service %s context done: %v", controlID, ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// sendLoop handles sending ProxyMessages to the control service
|
||||
func (s *Server) sendLoop(streamCtx *StreamContext, done chan<- error) {
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-streamCtx.sendChan:
|
||||
if !ok {
|
||||
done <- nil
|
||||
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
|
||||
return
|
||||
}
|
||||
|
||||
case <-streamCtx.ctx.Done():
|
||||
done <- streamCtx.ctx.Err()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different ControlMessage types
|
||||
switch m := controlMsg.Message.(type) {
|
||||
case *pb.ControlMessage_Event:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleControlEvent(streamCtx.ctx, m.Event); err != nil {
|
||||
log.Errorf("Failed to handle control event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *pb.ControlMessage_Command:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleControlCommand(streamCtx.ctx, m.Command); err != nil {
|
||||
log.Errorf("Failed to handle control command: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *pb.ControlMessage_Config:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleControlConfig(streamCtx.ctx, m.Config); err != nil {
|
||||
log.Errorf("Failed to handle control config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case *pb.ControlMessage_ExposedService:
|
||||
if s.handler != nil {
|
||||
if err := s.handler.HandleExposedServiceEvent(streamCtx.ctx, m.ExposedService); err != nil {
|
||||
log.Errorf("Failed to handle exposed service event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
log.Warnf("Received unknown control message type: %T", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendProxyMessage sends a ProxyMessage to all connected control services
|
||||
func (s *Server) SendProxyMessage(msg *pb.ProxyMessage) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, streamCtx := range s.streams {
|
||||
select {
|
||||
case streamCtx.sendChan <- msg:
|
||||
// Message queued successfully
|
||||
default:
|
||||
log.Warn("Send channel full, dropping message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveStreams returns the number of active streams
|
||||
func (s *Server) GetActiveStreams() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.streams)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/caarlos0/env/v11"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedToParseConfig = errors.New("failed to parse config from env")
|
||||
)
|
||||
|
||||
// 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"`
|
||||
|
||||
// WriteTimeout is the maximum duration before timing out writes of the response
|
||||
WriteTimeout time.Duration `env:"NB_PROXY_WRITE_TIMEOUT" envDefault:"30s" json:"write_timeout"`
|
||||
|
||||
// IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled
|
||||
IdleTimeout time.Duration `env:"NB_PROXY_IDLE_TIMEOUT" envDefault:"60s" json:"idle_timeout"`
|
||||
|
||||
// ShutdownTimeout is the maximum duration to wait for graceful shutdown
|
||||
ShutdownTimeout time.Duration `env:"NB_PROXY_SHUTDOWN_TIMEOUT" envDefault:"10s" json:"shutdown_timeout"`
|
||||
|
||||
// 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 string `env:"NB_PROXY_GRPC_LISTEN_ADDRESS" envDefault:":50051" json:"grpc_listen_address"`
|
||||
|
||||
// ProxyID is a unique identifier for this proxy instance
|
||||
ProxyID string `env:"NB_PROXY_ID" envDefault:"" json:"proxy_id"`
|
||||
|
||||
// EnableGRPC enables the gRPC control server
|
||||
EnableGRPC bool `env:"NB_PROXY_ENABLE_GRPC" envDefault:"false" json:"enable_grpc"`
|
||||
}
|
||||
|
||||
// ParseAndLoad parses configuration from environment variables
|
||||
func ParseAndLoad() (Config, error) {
|
||||
var cfg Config
|
||||
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return cfg, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return cfg, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadFromFile reads configuration from a JSON file
|
||||
func LoadFromFile(path string) (Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// If config file is provided, load it first
|
||||
if configPath != "" {
|
||||
fileCfg, err := LoadFromFile(configPath)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return Config{}, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
"warn": true,
|
||||
"error": true,
|
||||
}
|
||||
|
||||
if !validLogLevels[c.LogLevel] {
|
||||
return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", c.LogLevel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/internal/reverseproxy"
|
||||
grpcpkg "github.com/netbirdio/netbird/proxy/pkg/grpc"
|
||||
pb "github.com/netbirdio/netbird/proxy/pkg/grpc/proto"
|
||||
)
|
||||
|
||||
// Server represents the reverse proxy server with integrated gRPC control server
|
||||
type Server struct {
|
||||
config Config
|
||||
grpcServer *grpcpkg.Server
|
||||
caddyProxy *reverseproxy.CaddyProxy
|
||||
|
||||
mu sync.RWMutex
|
||||
isRunning bool
|
||||
grpcRunning bool
|
||||
|
||||
shutdownCtx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
|
||||
// Statistics for gRPC reporting
|
||||
stats *Stats
|
||||
|
||||
// Track exposed services and their peer configs
|
||||
exposedServices map[string]*ExposedServiceConfig
|
||||
}
|
||||
|
||||
// Stats holds proxy statistics
|
||||
type Stats struct {
|
||||
mu sync.RWMutex
|
||||
totalRequests uint64
|
||||
activeConns uint64
|
||||
bytesSent uint64
|
||||
bytesReceived uint64
|
||||
}
|
||||
|
||||
// ExposedServiceConfig holds the configuration for an exposed service
|
||||
type ExposedServiceConfig struct {
|
||||
ServiceID string
|
||||
PeerConfig *PeerConfig
|
||||
UpstreamConfig *UpstreamConfig
|
||||
}
|
||||
|
||||
// PeerConfig holds WireGuard peer configuration
|
||||
type PeerConfig struct {
|
||||
PeerID string
|
||||
PublicKey string
|
||||
AllowedIPs []string
|
||||
Endpoint string
|
||||
TunnelIP string // The WireGuard tunnel IP to route traffic to
|
||||
}
|
||||
|
||||
// UpstreamConfig holds reverse proxy upstream configuration
|
||||
type UpstreamConfig struct {
|
||||
Domain string
|
||||
PathMappings map[string]string // path -> port mapping (relative to tunnel IP)
|
||||
}
|
||||
|
||||
// NewServer creates a new reverse proxy server instance
|
||||
func NewServer(config Config) (*Server, error) {
|
||||
// Validate config
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
shutdownCtx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
server := &Server{
|
||||
config: config,
|
||||
isRunning: false,
|
||||
grpcRunning: false,
|
||||
shutdownCtx: shutdownCtx,
|
||||
cancelFunc: cancelFunc,
|
||||
stats: &Stats{},
|
||||
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
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": data.ServiceID,
|
||||
"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
|
||||
},
|
||||
}
|
||||
caddyProxy, err := reverseproxy.New(caddyConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Caddy proxy: %w", err)
|
||||
}
|
||||
server.caddyProxy = caddyProxy
|
||||
|
||||
// Create gRPC server if enabled
|
||||
if config.EnableGRPC && config.GRPCListenAddress != "" {
|
||||
grpcConfig := grpcpkg.Config{
|
||||
ListenAddr: config.GRPCListenAddress,
|
||||
Handler: server, // Server implements StreamHandler interface
|
||||
}
|
||||
server.grpcServer = grpcpkg.NewServer(grpcConfig)
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// Start starts the reverse proxy server and optionally the gRPC control server
|
||||
func (s *Server) Start() error {
|
||||
s.mu.Lock()
|
||||
if s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("server is already running")
|
||||
}
|
||||
s.isRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Infof("Starting Caddy reverse proxy server on %s", s.config.ListenAddress)
|
||||
|
||||
// Start Caddy proxy
|
||||
if err := s.caddyProxy.Start(); err != nil {
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("failed to start Caddy proxy: %w", err)
|
||||
}
|
||||
|
||||
// Start gRPC server if configured
|
||||
if s.grpcServer != nil {
|
||||
s.mu.Lock()
|
||||
s.grpcRunning = true
|
||||
s.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
log.Infof("Starting gRPC control server on %s", s.config.GRPCListenAddress)
|
||||
if err := s.grpcServer.Start(); err != nil {
|
||||
log.Errorf("gRPC server error: %v", err)
|
||||
s.mu.Lock()
|
||||
s.grpcRunning = false
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Send started event
|
||||
time.Sleep(100 * time.Millisecond) // Give gRPC server time to start
|
||||
s.sendProxyEvent(pb.ProxyEvent_STARTED, "Proxy server started")
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.AddRoute(
|
||||
&reverseproxy.RouteConfig{
|
||||
ID: "test",
|
||||
Domain: "test.netbird.io",
|
||||
PathMappings: map[string]string{"/": "localhost:8080"},
|
||||
}); 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
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
s.mu.Lock()
|
||||
if !s.isRunning {
|
||||
s.mu.Unlock()
|
||||
return fmt.Errorf("server is not running")
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Info("Shutting down servers gracefully...")
|
||||
|
||||
// If no context provided, use the server's shutdown timeout
|
||||
if ctx == nil {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(context.Background(), s.config.ShutdownTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
// Send stopped event before shutdown
|
||||
if s.grpcServer != nil && s.grpcRunning {
|
||||
s.sendProxyEvent(pb.ProxyEvent_STOPPED, "Proxy server shutting down")
|
||||
}
|
||||
|
||||
var caddyErr, grpcErr error
|
||||
|
||||
// Shutdown gRPC server first
|
||||
if s.grpcServer != nil && s.grpcRunning {
|
||||
if err := s.grpcServer.Stop(ctx); err != nil {
|
||||
grpcErr = fmt.Errorf("gRPC server shutdown failed: %w", err)
|
||||
log.Error(grpcErr)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.grpcRunning = false
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Shutdown Caddy proxy
|
||||
if err := s.caddyProxy.Stop(ctx); err != nil {
|
||||
caddyErr = fmt.Errorf("Caddy proxy shutdown failed: %w", err)
|
||||
log.Error(caddyErr)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.isRunning = false
|
||||
s.mu.Unlock()
|
||||
|
||||
if caddyErr != nil {
|
||||
return caddyErr
|
||||
}
|
||||
if grpcErr != nil {
|
||||
return grpcErr
|
||||
}
|
||||
|
||||
log.Info("All servers stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsRunning returns whether the server is currently running
|
||||
func (s *Server) IsRunning() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// GetConfig returns a copy of the server configuration
|
||||
func (s *Server) GetConfig() Config {
|
||||
return s.config
|
||||
}
|
||||
|
||||
// GetStats returns a copy of current statistics
|
||||
func (s *Server) GetStats() *pb.ProxyStats {
|
||||
s.stats.mu.RLock()
|
||||
defer s.stats.mu.RUnlock()
|
||||
|
||||
return &pb.ProxyStats{
|
||||
Timestamp: timestamppb.Now(),
|
||||
TotalRequests: s.stats.totalRequests,
|
||||
ActiveConnections: s.stats.activeConns,
|
||||
BytesSent: s.stats.bytesSent,
|
||||
BytesReceived: s.stats.bytesReceived,
|
||||
}
|
||||
}
|
||||
|
||||
// StreamHandler interface implementation
|
||||
|
||||
// HandleControlEvent handles incoming control events
|
||||
// This is where ExposedService events will be routed
|
||||
func (s *Server) HandleControlEvent(ctx context.Context, event *pb.ControlEvent) error {
|
||||
log.WithFields(log.Fields{
|
||||
"event_id": event.EventId,
|
||||
"message": event.Message,
|
||||
}).Info("Received control event")
|
||||
|
||||
// TODO: Parse event type and route to appropriate handler
|
||||
// if event.Type == "ExposedServiceCreated" {
|
||||
// return s.handleExposedServiceCreated(ctx, event)
|
||||
// } else if event.Type == "ExposedServiceUpdated" {
|
||||
// return s.handleExposedServiceUpdated(ctx, event)
|
||||
// } else if event.Type == "ExposedServiceRemoved" {
|
||||
// return s.handleExposedServiceRemoved(ctx, event)
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleControlCommand handles incoming control commands
|
||||
func (s *Server) HandleControlCommand(ctx context.Context, command *pb.ControlCommand) error {
|
||||
log.WithFields(log.Fields{
|
||||
"command_id": command.CommandId,
|
||||
"type": command.Type.String(),
|
||||
}).Info("Received control command")
|
||||
|
||||
switch command.Type {
|
||||
case pb.ControlCommand_GET_STATS:
|
||||
// Stats are automatically sent, just log
|
||||
log.Debug("Stats requested via command")
|
||||
case pb.ControlCommand_RELOAD_CONFIG:
|
||||
log.Info("Config reload requested (not implemented yet)")
|
||||
case pb.ControlCommand_ENABLE_DEBUG:
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.Info("Debug logging enabled")
|
||||
case pb.ControlCommand_DISABLE_DEBUG:
|
||||
log.SetLevel(log.InfoLevel)
|
||||
log.Info("Debug logging disabled")
|
||||
case pb.ControlCommand_SHUTDOWN:
|
||||
log.Warn("Shutdown command received")
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
s.cancelFunc() // Trigger graceful shutdown
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleControlConfig handles incoming configuration updates
|
||||
func (s *Server) HandleControlConfig(ctx context.Context, config *pb.ControlConfig) error {
|
||||
log.WithFields(log.Fields{
|
||||
"config_version": config.ConfigVersion,
|
||||
"settings": config.Settings,
|
||||
}).Info("Received config update")
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleExposedServiceEvent handles exposed service lifecycle events
|
||||
func (s *Server) HandleExposedServiceEvent(ctx context.Context, event *pb.ExposedServiceEvent) error {
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": event.ServiceId,
|
||||
"type": event.Type.String(),
|
||||
}).Info("Received exposed service event")
|
||||
|
||||
// Convert proto types to internal types
|
||||
peerConfig := &PeerConfig{
|
||||
PeerID: event.PeerConfig.PeerId,
|
||||
PublicKey: event.PeerConfig.PublicKey,
|
||||
AllowedIPs: event.PeerConfig.AllowedIps,
|
||||
Endpoint: event.PeerConfig.Endpoint,
|
||||
TunnelIP: event.PeerConfig.TunnelIp,
|
||||
}
|
||||
|
||||
upstreamConfig := &UpstreamConfig{
|
||||
Domain: event.UpstreamConfig.Domain,
|
||||
PathMappings: event.UpstreamConfig.PathMappings,
|
||||
}
|
||||
|
||||
// Route to appropriate handler based on event type
|
||||
switch event.Type {
|
||||
case pb.ExposedServiceEvent_CREATED:
|
||||
return s.handleExposedServiceCreated(event.ServiceId, peerConfig, upstreamConfig)
|
||||
|
||||
case pb.ExposedServiceEvent_UPDATED:
|
||||
return s.handleExposedServiceUpdated(event.ServiceId, peerConfig, upstreamConfig)
|
||||
|
||||
case pb.ExposedServiceEvent_REMOVED:
|
||||
return s.handleExposedServiceRemoved(event.ServiceId)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown exposed service event type: %v", event.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Exposed Service Handlers
|
||||
|
||||
// handleExposedServiceCreated handles the creation of a new exposed service
|
||||
func (s *Server) handleExposedServiceCreated(serviceID string, peerConfig *PeerConfig, upstreamConfig *UpstreamConfig) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if service already exists
|
||||
if _, exists := s.exposedServices[serviceID]; exists {
|
||||
return fmt.Errorf("exposed service %s already exists", serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": serviceID,
|
||||
"peer_id": peerConfig.PeerID,
|
||||
"tunnel_ip": peerConfig.TunnelIP,
|
||||
"domain": upstreamConfig.Domain,
|
||||
}).Info("Creating exposed service")
|
||||
|
||||
// TODO: Create WireGuard tunnel for peer
|
||||
// 1. Initialize WireGuard interface if not already done
|
||||
// 2. Add peer configuration:
|
||||
// - Public key: peerConfig.PublicKey
|
||||
// - Endpoint: peerConfig.Endpoint
|
||||
// - Allowed IPs: peerConfig.AllowedIPs
|
||||
// - Persistent keepalive: 25 seconds
|
||||
// 3. Bring up the WireGuard interface
|
||||
// 4. Verify tunnel connectivity to peerConfig.TunnelIP
|
||||
// Example pseudo-code:
|
||||
// wgClient.AddPeer(&wireguard.PeerConfig{
|
||||
// PublicKey: peerConfig.PublicKey,
|
||||
// Endpoint: peerConfig.Endpoint,
|
||||
// AllowedIPs: peerConfig.AllowedIPs,
|
||||
// PersistentKeepalive: 25,
|
||||
// })
|
||||
|
||||
// Build path mappings with tunnel IP
|
||||
pathMappings := make(map[string]string)
|
||||
for path, port := range upstreamConfig.PathMappings {
|
||||
// Combine tunnel IP with port
|
||||
target := fmt.Sprintf("%s:%s", peerConfig.TunnelIP, port)
|
||||
pathMappings[path] = target
|
||||
}
|
||||
|
||||
// Add route to Caddy
|
||||
route := &reverseproxy.RouteConfig{
|
||||
ID: serviceID,
|
||||
Domain: upstreamConfig.Domain,
|
||||
PathMappings: pathMappings,
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.AddRoute(route); err != nil {
|
||||
return fmt.Errorf("failed to add route: %w", err)
|
||||
}
|
||||
|
||||
// Store service config
|
||||
s.exposedServices[serviceID] = &ExposedServiceConfig{
|
||||
ServiceID: serviceID,
|
||||
PeerConfig: peerConfig,
|
||||
UpstreamConfig: upstreamConfig,
|
||||
}
|
||||
|
||||
log.Infof("Exposed service %s created successfully", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleExposedServiceUpdated handles updates to an existing exposed service
|
||||
func (s *Server) handleExposedServiceUpdated(serviceID string, peerConfig *PeerConfig, upstreamConfig *UpstreamConfig) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if service exists
|
||||
if _, exists := s.exposedServices[serviceID]; !exists {
|
||||
return fmt.Errorf("exposed service %s not found", serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": serviceID,
|
||||
"peer_id": peerConfig.PeerID,
|
||||
"tunnel_ip": peerConfig.TunnelIP,
|
||||
"domain": upstreamConfig.Domain,
|
||||
}).Info("Updating exposed service")
|
||||
|
||||
// TODO: Update WireGuard tunnel if peer config changed
|
||||
|
||||
// Build path mappings with tunnel IP
|
||||
pathMappings := make(map[string]string)
|
||||
for path, port := range upstreamConfig.PathMappings {
|
||||
target := fmt.Sprintf("%s:%s", peerConfig.TunnelIP, port)
|
||||
pathMappings[path] = target
|
||||
}
|
||||
|
||||
// Update route in Caddy
|
||||
route := &reverseproxy.RouteConfig{
|
||||
ID: serviceID,
|
||||
Domain: upstreamConfig.Domain,
|
||||
PathMappings: pathMappings,
|
||||
}
|
||||
|
||||
if err := s.caddyProxy.UpdateRoute(route); err != nil {
|
||||
return fmt.Errorf("failed to update route: %w", err)
|
||||
}
|
||||
|
||||
// Update service config
|
||||
s.exposedServices[serviceID] = &ExposedServiceConfig{
|
||||
ServiceID: serviceID,
|
||||
PeerConfig: peerConfig,
|
||||
UpstreamConfig: upstreamConfig,
|
||||
}
|
||||
|
||||
log.Infof("Exposed service %s updated successfully", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleExposedServiceRemoved handles the removal of an exposed service
|
||||
func (s *Server) handleExposedServiceRemoved(serviceID string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Check if service exists
|
||||
if _, exists := s.exposedServices[serviceID]; !exists {
|
||||
return fmt.Errorf("exposed service %s not found", serviceID)
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"service_id": serviceID,
|
||||
}).Info("Removing exposed service")
|
||||
|
||||
// Remove route from Caddy
|
||||
if err := s.caddyProxy.RemoveRoute(serviceID); err != nil {
|
||||
return fmt.Errorf("failed to remove route: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Remove WireGuard tunnel for peer
|
||||
|
||||
// Remove service config
|
||||
delete(s.exposedServices, serviceID)
|
||||
|
||||
log.Infof("Exposed service %s removed successfully", serviceID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListExposedServices returns a list of all exposed service IDs
|
||||
func (s *Server) ListExposedServices() []string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
services := make([]string, 0, len(s.exposedServices))
|
||||
for id := range s.exposedServices {
|
||||
services = append(services, id)
|
||||
}
|
||||
return services
|
||||
}
|
||||
|
||||
// GetExposedService returns the configuration for a specific exposed service
|
||||
func (s *Server) GetExposedService(serviceID string) (*ExposedServiceConfig, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
service, exists := s.exposedServices[serviceID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("exposed service %s not found", serviceID)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (s *Server) sendProxyEvent(eventType pb.ProxyEvent_EventType, message string) {
|
||||
// This would typically be called to send events
|
||||
// The actual sending happens via the gRPC stream
|
||||
log.WithFields(log.Fields{
|
||||
"type": eventType.String(),
|
||||
"message": message,
|
||||
}).Debug("Proxy event")
|
||||
}
|
||||
|
||||
// Stats methods
|
||||
|
||||
func (s *Stats) IncrementRequests() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.totalRequests++
|
||||
}
|
||||
|
||||
func (s *Stats) IncrementActiveConns() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.activeConns++
|
||||
}
|
||||
|
||||
func (s *Stats) DecrementActiveConns() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.activeConns > 0 {
|
||||
s.activeConns--
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stats) AddBytesSent(bytes uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.bytesSent += bytes
|
||||
}
|
||||
|
||||
func (s *Stats) AddBytesReceived(bytes uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.bytesReceived += bytes
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version is the application version (set via ldflags during build)
|
||||
Version = "dev"
|
||||
|
||||
// Commit is the git commit hash (set via ldflags during build)
|
||||
Commit = "unknown"
|
||||
|
||||
// BuildDate is the build date (set via ldflags during build)
|
||||
BuildDate = "unknown"
|
||||
|
||||
// GoVersion is the Go version used to build the binary
|
||||
GoVersion = runtime.Version()
|
||||
)
|
||||
|
||||
// Info contains version information
|
||||
type Info struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
BuildDate string `json:"build_date"`
|
||||
GoVersion string `json:"go_version"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
}
|
||||
|
||||
// Get returns the version information
|
||||
func Get() Info {
|
||||
return Info{
|
||||
Version: Version,
|
||||
Commit: Commit,
|
||||
BuildDate: BuildDate,
|
||||
GoVersion: GoVersion,
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a formatted version string
|
||||
func String() string {
|
||||
return fmt.Sprintf("Version: %s, Commit: %s, BuildDate: %s, Go: %s",
|
||||
Version, Commit, BuildDate, GoVersion)
|
||||
}
|
||||
|
||||
// Short returns a short version string
|
||||
func Short() string {
|
||||
if Version == "dev" {
|
||||
return fmt.Sprintf("%s (%s)", Version, Commit[:7])
|
||||
}
|
||||
return Version
|
||||
}
|
||||
Reference in New Issue
Block a user