mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
252 lines
8.1 KiB
Go
252 lines
8.1 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
pb "github.com/golang/protobuf/proto" // nolint
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/encryption"
|
|
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
|
nbContext "github.com/netbirdio/netbird/management/server/context"
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/store"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
internalStatus "github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
// CreateExpose handles a peer request to create a new expose service.
|
|
func (s *Server) CreateExpose(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
|
exposeReq := &proto.ExposeServiceRequest{}
|
|
peerKey, err := s.parseRequest(ctx, req, exposeReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
accountID, peer, err := s.authenticateExposePeer(ctx, peerKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// nolint:staticcheck
|
|
ctx = context.WithValue(ctx, nbContext.AccountIDKey, accountID)
|
|
|
|
reverseProxyMgr := s.getReverseProxyManager()
|
|
if reverseProxyMgr == nil {
|
|
return nil, status.Errorf(codes.Internal, "reverse proxy manager not available")
|
|
}
|
|
|
|
if exposeReq.Port > 65535 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "port out of range: %d", exposeReq.Port)
|
|
}
|
|
if exposeReq.ListenPort > 65535 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "listen_port out of range: %d", exposeReq.ListenPort)
|
|
}
|
|
|
|
mode, err := exposeProtocolToString(exposeReq.Protocol)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
|
|
}
|
|
|
|
created, err := reverseProxyMgr.CreateServiceFromPeer(ctx, accountID, peer.ID, &rpservice.ExposeServiceRequest{
|
|
NamePrefix: exposeReq.NamePrefix,
|
|
Port: uint16(exposeReq.Port), //nolint:gosec // validated above
|
|
Mode: mode,
|
|
TargetProtocol: exposeTargetProtocol(exposeReq.Protocol),
|
|
Domain: exposeReq.Domain,
|
|
Pin: exposeReq.Pin,
|
|
Password: exposeReq.Password,
|
|
UserGroups: exposeReq.UserGroups,
|
|
ListenPort: uint16(exposeReq.ListenPort), //nolint:gosec // validated above
|
|
})
|
|
if err != nil {
|
|
return nil, mapExposeError(ctx, err)
|
|
}
|
|
|
|
return s.encryptResponse(peerKey, &proto.ExposeServiceResponse{
|
|
ServiceName: created.ServiceName,
|
|
ServiceUrl: created.ServiceURL,
|
|
Domain: created.Domain,
|
|
PortAutoAssigned: created.PortAutoAssigned,
|
|
})
|
|
}
|
|
|
|
// RenewExpose extends the TTL of an active expose session.
|
|
func (s *Server) RenewExpose(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
|
renewReq := &proto.RenewExposeRequest{}
|
|
peerKey, err := s.parseRequest(ctx, req, renewReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
accountID, peer, err := s.authenticateExposePeer(ctx, peerKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reverseProxyMgr := s.getReverseProxyManager()
|
|
if reverseProxyMgr == nil {
|
|
return nil, status.Errorf(codes.Internal, "reverse proxy manager not available")
|
|
}
|
|
|
|
serviceID, err := s.resolveServiceID(ctx, renewReq.Domain)
|
|
if err != nil {
|
|
return nil, mapExposeError(ctx, err)
|
|
}
|
|
|
|
if err := reverseProxyMgr.RenewServiceFromPeer(ctx, accountID, peer.ID, serviceID); err != nil {
|
|
return nil, mapExposeError(ctx, err)
|
|
}
|
|
|
|
return s.encryptResponse(peerKey, &proto.RenewExposeResponse{})
|
|
}
|
|
|
|
// StopExpose terminates an active expose session.
|
|
func (s *Server) StopExpose(ctx context.Context, req *proto.EncryptedMessage) (*proto.EncryptedMessage, error) {
|
|
stopReq := &proto.StopExposeRequest{}
|
|
peerKey, err := s.parseRequest(ctx, req, stopReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
accountID, peer, err := s.authenticateExposePeer(ctx, peerKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reverseProxyMgr := s.getReverseProxyManager()
|
|
if reverseProxyMgr == nil {
|
|
return nil, status.Errorf(codes.Internal, "reverse proxy manager not available")
|
|
}
|
|
|
|
serviceID, err := s.resolveServiceID(ctx, stopReq.Domain)
|
|
if err != nil {
|
|
return nil, mapExposeError(ctx, err)
|
|
}
|
|
|
|
if err := reverseProxyMgr.StopServiceFromPeer(ctx, accountID, peer.ID, serviceID); err != nil {
|
|
return nil, mapExposeError(ctx, err)
|
|
}
|
|
|
|
return s.encryptResponse(peerKey, &proto.StopExposeResponse{})
|
|
}
|
|
|
|
func mapExposeError(ctx context.Context, err error) error {
|
|
s, ok := internalStatus.FromError(err)
|
|
if !ok {
|
|
log.WithContext(ctx).Errorf("expose service error: %v", err)
|
|
return status.Errorf(codes.Internal, "internal error")
|
|
}
|
|
|
|
switch s.Type() {
|
|
case internalStatus.InvalidArgument:
|
|
return status.Errorf(codes.InvalidArgument, "%s", s.Message)
|
|
case internalStatus.PermissionDenied:
|
|
return status.Errorf(codes.PermissionDenied, "%s", s.Message)
|
|
case internalStatus.NotFound:
|
|
return status.Errorf(codes.NotFound, "%s", s.Message)
|
|
case internalStatus.AlreadyExists:
|
|
return status.Errorf(codes.AlreadyExists, "%s", s.Message)
|
|
case internalStatus.PreconditionFailed:
|
|
return status.Errorf(codes.ResourceExhausted, "%s", s.Message)
|
|
default:
|
|
log.WithContext(ctx).Errorf("expose service error: %v", err)
|
|
return status.Errorf(codes.Internal, "internal error")
|
|
}
|
|
}
|
|
|
|
func (s *Server) encryptResponse(peerKey wgtypes.Key, msg pb.Message) (*proto.EncryptedMessage, error) {
|
|
wgKey, err := s.secretsManager.GetWGKey()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "internal error")
|
|
}
|
|
|
|
encryptedResp, err := encryption.EncryptMessage(peerKey, wgKey, msg)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "encrypt response")
|
|
}
|
|
|
|
return &proto.EncryptedMessage{
|
|
WgPubKey: wgKey.PublicKey().String(),
|
|
Body: encryptedResp,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) authenticateExposePeer(ctx context.Context, peerKey wgtypes.Key) (string, *nbpeer.Peer, error) {
|
|
accountID, err := s.accountManager.GetAccountIDForPeerKey(ctx, peerKey.String())
|
|
if err != nil {
|
|
if errStatus, ok := internalStatus.FromError(err); ok && errStatus.Type() == internalStatus.NotFound {
|
|
return "", nil, status.Errorf(codes.PermissionDenied, "peer is not registered")
|
|
}
|
|
return "", nil, status.Errorf(codes.Internal, "lookup account for peer")
|
|
}
|
|
|
|
peer, err := s.accountManager.GetStore().GetPeerByPeerPubKey(ctx, store.LockingStrengthNone, peerKey.String())
|
|
if err != nil {
|
|
return "", nil, status.Errorf(codes.PermissionDenied, "peer is not registered")
|
|
}
|
|
|
|
return accountID, peer, nil
|
|
}
|
|
|
|
func (s *Server) getReverseProxyManager() rpservice.Manager {
|
|
s.reverseProxyMu.RLock()
|
|
defer s.reverseProxyMu.RUnlock()
|
|
return s.reverseProxyManager
|
|
}
|
|
|
|
// SetReverseProxyManager sets the reverse proxy manager on the server.
|
|
func (s *Server) SetReverseProxyManager(mgr rpservice.Manager) {
|
|
s.reverseProxyMu.Lock()
|
|
defer s.reverseProxyMu.Unlock()
|
|
s.reverseProxyManager = mgr
|
|
}
|
|
|
|
// resolveServiceID looks up the service by its globally unique domain.
|
|
func (s *Server) resolveServiceID(ctx context.Context, domain string) (string, error) {
|
|
if domain == "" {
|
|
return "", status.Errorf(codes.InvalidArgument, "domain is required")
|
|
}
|
|
|
|
svc, err := s.accountManager.GetStore().GetServiceByDomain(ctx, domain)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return svc.ID, nil
|
|
}
|
|
|
|
func exposeProtocolToString(p proto.ExposeProtocol) (string, error) {
|
|
switch p {
|
|
case proto.ExposeProtocol_EXPOSE_HTTP, proto.ExposeProtocol_EXPOSE_HTTPS:
|
|
return "http", nil
|
|
case proto.ExposeProtocol_EXPOSE_TCP:
|
|
return "tcp", nil
|
|
case proto.ExposeProtocol_EXPOSE_UDP:
|
|
return "udp", nil
|
|
case proto.ExposeProtocol_EXPOSE_TLS:
|
|
return "tls", nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported expose protocol: %v", p)
|
|
}
|
|
}
|
|
|
|
// exposeTargetProtocol returns the target protocol for the given expose protocol.
|
|
// For HTTP mode, this is http or https (the scheme used to connect to the backend).
|
|
// For L4 modes, this is tcp or udp (the transport used to connect to the backend).
|
|
func exposeTargetProtocol(p proto.ExposeProtocol) string {
|
|
switch p {
|
|
case proto.ExposeProtocol_EXPOSE_HTTPS:
|
|
return rpservice.TargetProtoHTTPS
|
|
case proto.ExposeProtocol_EXPOSE_TCP, proto.ExposeProtocol_EXPOSE_TLS:
|
|
return rpservice.TargetProtoTCP
|
|
case proto.ExposeProtocol_EXPOSE_UDP:
|
|
return rpservice.TargetProtoUDP
|
|
default:
|
|
return rpservice.TargetProtoHTTP
|
|
}
|
|
}
|