trying embedded caddy reverse proxy

This commit is contained in:
pascal
2026-01-14 17:16:42 +01:00
parent d9118eb239
commit 626e892e3b
30 changed files with 6978 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+159
View File
@@ -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;
}
+137
View File
@@ -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",
}
+286
View File
@@ -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)
}