[client,management] Feature/client service expose (#5411)

CLI: new expose command to publish a local port with flags for PIN, password, user groups, custom domain, name prefix and protocol (HTTP default).
Management/API: create/renew/stop expose sessions (streamed status), automatic naming/domain, TTL renewals, background expiration, new management RPCs and client methods.
UI/API: account settings now include peer_expose_enabled and peer_expose_groups; new activity codes for peer expose events.
This commit is contained in:
Maycon Santos
2026-02-24 10:02:16 +01:00
committed by GitHub
parent 37f025c966
commit 63c83aa8d2
44 changed files with 3867 additions and 422 deletions

View File

@@ -11,6 +11,7 @@ import (
"github.com/netbirdio/netbird/shared/management/proto"
)
// Client is the interface for the management service client.
type Client interface {
io.Closer
Sync(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
@@ -24,4 +25,7 @@ type Client interface {
IsHealthy() bool
SyncMeta(sysInfo *system.Info) error
Logout() error
CreateExpose(ctx context.Context, req ExposeRequest) (*ExposeResponse, error)
RenewExpose(ctx context.Context, domain string) error
StopExpose(ctx context.Context, domain string) error
}

View File

@@ -48,6 +48,22 @@ type GrpcClient struct {
connStateCallbackLock sync.RWMutex
}
type ExposeRequest struct {
NamePrefix string
Domain string
Port uint16
Protocol int
Pin string
Password string
UserGroups []string
}
type ExposeResponse struct {
ServiceName string
Domain string
ServiceURL string
}
// NewClient creates a new client to Management service
func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsEnabled bool) (*GrpcClient, error) {
var conn *grpc.ClientConn
@@ -690,6 +706,123 @@ func (c *GrpcClient) Logout() error {
return nil
}
// CreateExpose calls the management server to create a new expose service.
func (c *GrpcClient) CreateExpose(ctx context.Context, req ExposeRequest) (*ExposeResponse, error) {
serverPubKey, err := c.GetServerPublicKey()
if err != nil {
return nil, err
}
protoReq, err := toProtoExposeServiceRequest(req)
if err != nil {
return nil, err
}
encReq, err := encryption.EncryptMessage(*serverPubKey, c.key, protoReq)
if err != nil {
return nil, fmt.Errorf("encrypt create expose request: %w", err)
}
mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout)
defer cancel()
resp, err := c.realClient.CreateExpose(mgmCtx, &proto.EncryptedMessage{
WgPubKey: c.key.PublicKey().String(),
Body: encReq,
})
if err != nil {
return nil, err
}
exposeResp := &proto.ExposeServiceResponse{}
if err := encryption.DecryptMessage(*serverPubKey, c.key, resp.Body, exposeResp); err != nil {
return nil, fmt.Errorf("decrypt create expose response: %w", err)
}
return fromProtoExposeResponse(exposeResp), nil
}
// RenewExpose extends the TTL of an active expose session on the management server.
func (c *GrpcClient) RenewExpose(ctx context.Context, domain string) error {
serverPubKey, err := c.GetServerPublicKey()
if err != nil {
return err
}
req := &proto.RenewExposeRequest{Domain: domain}
encReq, err := encryption.EncryptMessage(*serverPubKey, c.key, req)
if err != nil {
return fmt.Errorf("encrypt renew expose request: %w", err)
}
mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout)
defer cancel()
_, err = c.realClient.RenewExpose(mgmCtx, &proto.EncryptedMessage{
WgPubKey: c.key.PublicKey().String(),
Body: encReq,
})
return err
}
// StopExpose terminates an active expose session on the management server.
func (c *GrpcClient) StopExpose(ctx context.Context, domain string) error {
serverPubKey, err := c.GetServerPublicKey()
if err != nil {
return err
}
req := &proto.StopExposeRequest{Domain: domain}
encReq, err := encryption.EncryptMessage(*serverPubKey, c.key, req)
if err != nil {
return fmt.Errorf("encrypt stop expose request: %w", err)
}
mgmCtx, cancel := context.WithTimeout(ctx, ConnectTimeout)
defer cancel()
_, err = c.realClient.StopExpose(mgmCtx, &proto.EncryptedMessage{
WgPubKey: c.key.PublicKey().String(),
Body: encReq,
})
return err
}
func fromProtoExposeResponse(resp *proto.ExposeServiceResponse) *ExposeResponse {
return &ExposeResponse{
ServiceName: resp.ServiceName,
Domain: resp.Domain,
ServiceURL: resp.ServiceUrl,
}
}
func toProtoExposeServiceRequest(req ExposeRequest) (*proto.ExposeServiceRequest, error) {
var protocol proto.ExposeProtocol
switch req.Protocol {
case int(proto.ExposeProtocol_EXPOSE_HTTP):
protocol = proto.ExposeProtocol_EXPOSE_HTTP
case int(proto.ExposeProtocol_EXPOSE_HTTPS):
protocol = proto.ExposeProtocol_EXPOSE_HTTPS
case int(proto.ExposeProtocol_EXPOSE_TCP):
protocol = proto.ExposeProtocol_EXPOSE_TCP
case int(proto.ExposeProtocol_EXPOSE_UDP):
protocol = proto.ExposeProtocol_EXPOSE_UDP
default:
return nil, fmt.Errorf("invalid expose protocol: %d", req.Protocol)
}
return &proto.ExposeServiceRequest{
NamePrefix: req.NamePrefix,
Domain: req.Domain,
Port: uint32(req.Port),
Protocol: protocol,
Pin: req.Pin,
Password: req.Password,
UserGroups: req.UserGroups,
}, nil
}
func infoToMetaData(info *system.Info) *proto.PeerSystemMeta {
if info == nil {
return nil

View File

@@ -10,6 +10,7 @@ import (
"github.com/netbirdio/netbird/shared/management/proto"
)
// MockClient is a mock implementation of the Client interface for testing.
type MockClient struct {
CloseFunc func() error
SyncFunc func(ctx context.Context, sysInfo *system.Info, msgHandler func(msg *proto.SyncResponse) error) error
@@ -21,6 +22,9 @@ type MockClient struct {
SyncMetaFunc func(sysInfo *system.Info) error
LogoutFunc func() error
JobFunc func(ctx context.Context, msgHandler func(msg *proto.JobRequest) *proto.JobResponse) error
CreateExposeFunc func(ctx context.Context, req ExposeRequest) (*ExposeResponse, error)
RenewExposeFunc func(ctx context.Context, domain string) error
StopExposeFunc func(ctx context.Context, domain string) error
}
func (m *MockClient) IsHealthy() bool {
@@ -80,10 +84,10 @@ func (m *MockClient) GetPKCEAuthorizationFlow(serverKey wgtypes.Key) (*proto.PKC
if m.GetPKCEAuthorizationFlowFunc == nil {
return nil, nil
}
return m.GetPKCEAuthorizationFlow(serverKey)
return m.GetPKCEAuthorizationFlowFunc(serverKey)
}
// GetNetworkMap mock implementation of GetNetworkMap from mgm.Client interface
// GetNetworkMap mock implementation of GetNetworkMap from Client interface.
func (m *MockClient) GetNetworkMap(_ *system.Info) (*proto.NetworkMap, error) {
return nil, nil
}
@@ -101,3 +105,24 @@ func (m *MockClient) Logout() error {
}
return m.LogoutFunc()
}
func (m *MockClient) CreateExpose(ctx context.Context, req ExposeRequest) (*ExposeResponse, error) {
if m.CreateExposeFunc == nil {
return nil, nil
}
return m.CreateExposeFunc(ctx, req)
}
func (m *MockClient) RenewExpose(ctx context.Context, domain string) error {
if m.RenewExposeFunc == nil {
return nil
}
return m.RenewExposeFunc(ctx, domain)
}
func (m *MockClient) StopExpose(ctx context.Context, domain string) error {
if m.StopExposeFunc == nil {
return nil
}
return m.StopExposeFunc(ctx, domain)
}