diff --git a/client/cmd/expose.go b/client/cmd/expose.go new file mode 100644 index 000000000..991d3ab86 --- /dev/null +++ b/client/cmd/expose.go @@ -0,0 +1,194 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "os/signal" + "regexp" + "strconv" + "strings" + "syscall" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/client/proto" + "github.com/netbirdio/netbird/util" +) + +var pinRegexp = regexp.MustCompile(`^\d{6}$`) + +var ( + exposePin string + exposePassword string + exposeUserGroups []string + exposeDomain string + exposeNamePrefix string + exposeProtocol string +) + +var exposeCmd = &cobra.Command{ + Use: "expose ", + Short: "Expose a local port via the NetBird reverse proxy", + Args: cobra.ExactArgs(1), + Example: "netbird expose --with-password safe-pass 8080", + RunE: exposeFn, +} + +func init() { + exposeCmd.Flags().StringVar(&exposePin, "with-pin", "", "Protect the exposed service with a 6-digit PIN (e.g. --with-pin 123456)") + exposeCmd.Flags().StringVar(&exposePassword, "with-password", "", "Protect the exposed service with a password (e.g. --with-password my-secret)") + exposeCmd.Flags().StringSliceVar(&exposeUserGroups, "with-user-groups", nil, "Restrict access to specific user groups with SSO (e.g. --with-user-groups devops,Backend)") + exposeCmd.Flags().StringVar(&exposeDomain, "with-custom-domain", "", "Custom domain for the exposed service, must be configured to your account (e.g. --with-custom-domain myapp.example.com)") + exposeCmd.Flags().StringVar(&exposeNamePrefix, "with-name-prefix", "", "Prefix for the generated service name (e.g. --with-name-prefix my-app)") + exposeCmd.Flags().StringVar(&exposeProtocol, "protocol", "http", "Protocol to use, http/https is supported (e.g. --protocol http)") +} + +func validateExposeFlags(cmd *cobra.Command, portStr string) (uint64, error) { + port, err := strconv.ParseUint(portStr, 10, 32) + if err != nil { + return 0, fmt.Errorf("invalid port number: %s", portStr) + } + if port == 0 || port > 65535 { + return 0, fmt.Errorf("invalid port number: must be between 1 and 65535") + } + + if !isProtocolValid(exposeProtocol) { + return 0, fmt.Errorf("unsupported protocol %q: only 'http' or 'https' are supported", exposeProtocol) + } + + if exposePin != "" && !pinRegexp.MatchString(exposePin) { + return 0, fmt.Errorf("invalid pin: must be exactly 6 digits") + } + + if cmd.Flags().Changed("with-password") && exposePassword == "" { + return 0, fmt.Errorf("password cannot be empty") + } + + if cmd.Flags().Changed("with-user-groups") && len(exposeUserGroups) == 0 { + return 0, fmt.Errorf("user groups cannot be empty") + } + + return port, nil +} + +func isProtocolValid(exposeProtocol string) bool { + return strings.ToLower(exposeProtocol) == "http" || strings.ToLower(exposeProtocol) == "https" +} + +func exposeFn(cmd *cobra.Command, args []string) error { + SetFlagsFromEnvVars(rootCmd) + + if err := util.InitLog(logLevel, util.LogConsole); err != nil { + log.Errorf("failed initializing log %v", err) + return err + } + + cmd.Root().SilenceUsage = false + + port, err := validateExposeFlags(cmd, args[0]) + if err != nil { + return err + } + + cmd.Root().SilenceUsage = true + + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + cancel() + }() + + conn, err := DialClientGRPCServer(ctx, daemonAddr) + if err != nil { + return fmt.Errorf("connect to daemon: %w", err) + } + defer func() { + if err := conn.Close(); err != nil { + log.Debugf("failed to close daemon connection: %v", err) + } + }() + + client := proto.NewDaemonServiceClient(conn) + + protocol, err := toExposeProtocol(exposeProtocol) + if err != nil { + return err + } + + stream, err := client.ExposeService(ctx, &proto.ExposeServiceRequest{ + Port: uint32(port), + Protocol: protocol, + Pin: exposePin, + Password: exposePassword, + UserGroups: exposeUserGroups, + Domain: exposeDomain, + NamePrefix: exposeNamePrefix, + }) + if err != nil { + return fmt.Errorf("expose service: %w", err) + } + + if err := handleExposeReady(cmd, stream, port); err != nil { + return err + } + + return waitForExposeEvents(cmd, ctx, stream) +} + +func toExposeProtocol(exposeProtocol string) (proto.ExposeProtocol, error) { + switch strings.ToLower(exposeProtocol) { + case "http": + return proto.ExposeProtocol_EXPOSE_HTTP, nil + case "https": + return proto.ExposeProtocol_EXPOSE_HTTPS, nil + default: + return 0, fmt.Errorf("unsupported protocol %q: only 'http' or 'https' are supported", exposeProtocol) + } +} + +func handleExposeReady(cmd *cobra.Command, stream proto.DaemonService_ExposeServiceClient, port uint64) error { + event, err := stream.Recv() + if err != nil { + return fmt.Errorf("receive expose event: %w", err) + } + + switch e := event.Event.(type) { + case *proto.ExposeServiceEvent_Ready: + cmd.Println("Service exposed successfully!") + cmd.Printf(" Name: %s\n", e.Ready.ServiceName) + cmd.Printf(" URL: %s\n", e.Ready.ServiceUrl) + cmd.Printf(" Domain: %s\n", e.Ready.Domain) + cmd.Printf(" Protocol: %s\n", exposeProtocol) + cmd.Printf(" Port: %d\n", port) + cmd.Println() + cmd.Println("Press Ctrl+C to stop exposing.") + return nil + default: + return fmt.Errorf("unexpected expose event: %T", event.Event) + } +} + +func waitForExposeEvents(cmd *cobra.Command, ctx context.Context, stream proto.DaemonService_ExposeServiceClient) error { + for { + _, err := stream.Recv() + if err != nil { + if ctx.Err() != nil { + cmd.Println("\nService stopped.") + //nolint:nilerr + return nil + } + if errors.Is(err, io.EOF) { + return fmt.Errorf("connection to daemon closed unexpectedly") + } + return fmt.Errorf("stream error: %w", err) + } + } +} diff --git a/client/cmd/root.go b/client/cmd/root.go index f4f4f6052..961abd54e 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -144,6 +144,7 @@ func init() { rootCmd.AddCommand(forwardingRulesCmd) rootCmd.AddCommand(debugCmd) rootCmd.AddCommand(profileCmd) + rootCmd.AddCommand(exposeCmd) networksCMD.AddCommand(routesListCmd) networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd) diff --git a/client/iface/configurer/uapi.go b/client/iface/configurer/uapi.go index f85c7852a..d9bd9bfab 100644 --- a/client/iface/configurer/uapi.go +++ b/client/iface/configurer/uapi.go @@ -5,20 +5,18 @@ package configurer import ( "net" - log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/ipc" ) func openUAPI(deviceName string) (net.Listener, error) { uapiSock, err := ipc.UAPIOpen(deviceName) if err != nil { - log.Errorf("failed to open uapi socket: %v", err) return nil, err } listener, err := ipc.UAPIListen(deviceName, uapiSock) if err != nil { - log.Errorf("failed to listen on uapi socket: %v", err) + _ = uapiSock.Close() return nil, err } diff --git a/client/iface/configurer/usp.go b/client/iface/configurer/usp.go index 1298c609d..e3a96590c 100644 --- a/client/iface/configurer/usp.go +++ b/client/iface/configurer/usp.go @@ -54,6 +54,14 @@ func NewUSPConfigurer(device *device.Device, deviceName string, activityRecorder return wgCfg } +func NewUSPConfigurerNoUAPI(device *device.Device, deviceName string, activityRecorder *bind.ActivityRecorder) *WGUSPConfigurer { + return &WGUSPConfigurer{ + device: device, + deviceName: deviceName, + activityRecorder: activityRecorder, + } +} + func (c *WGUSPConfigurer) ConfigureInterface(privateKey string, port int) error { log.Debugf("adding Wireguard private key") key, err := wgtypes.ParseKey(privateKey) diff --git a/client/iface/device/device_netstack.go b/client/iface/device/device_netstack.go index e457657f7..1a92b148f 100644 --- a/client/iface/device/device_netstack.go +++ b/client/iface/device/device_netstack.go @@ -79,7 +79,7 @@ func (t *TunNetstackDevice) create() (WGConfigurer, error) { device.NewLogger(wgLogLevel(), "[netbird] "), ) - t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.bind.ActivityRecorder()) + t.configurer = configurer.NewUSPConfigurerNoUAPI(t.device, t.name, t.bind.ActivityRecorder()) err = t.configurer.ConfigureInterface(t.key, t.port) if err != nil { if cErr := tunIface.Close(); cErr != nil { diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index 9b7a7b52b..4a8cf8cec 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -277,7 +277,7 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr } } - log.Infof("added %d NRPT rules for %d domains. Domain list: %v", ruleIndex, len(domains), domains) + log.Infof("added %d NRPT rules for %d domains", ruleIndex, len(domains)) return ruleIndex, nil } diff --git a/client/internal/engine.go b/client/internal/engine.go index f2d724aa4..b0ae841f8 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -36,6 +36,7 @@ import ( "github.com/netbirdio/netbird/client/internal/dns" dnsconfig "github.com/netbirdio/netbird/client/internal/dns/config" "github.com/netbirdio/netbird/client/internal/dnsfwd" + "github.com/netbirdio/netbird/client/internal/expose" "github.com/netbirdio/netbird/client/internal/ingressgw" "github.com/netbirdio/netbird/client/internal/netflow" nftypes "github.com/netbirdio/netbird/client/internal/netflow/types" @@ -53,13 +54,11 @@ import ( "github.com/netbirdio/netbird/client/internal/updatemanager" "github.com/netbirdio/netbird/client/jobexec" cProto "github.com/netbirdio/netbird/client/proto" - "github.com/netbirdio/netbird/shared/management/domain" - semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" - "github.com/netbirdio/netbird/client/system" nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/route" mgm "github.com/netbirdio/netbird/shared/management/client" + "github.com/netbirdio/netbird/shared/management/domain" mgmProto "github.com/netbirdio/netbird/shared/management/proto" auth "github.com/netbirdio/netbird/shared/relay/auth/hmac" relayClient "github.com/netbirdio/netbird/shared/relay/client" @@ -75,7 +74,6 @@ import ( const ( PeerConnectionTimeoutMax = 45000 // ms PeerConnectionTimeoutMin = 30000 // ms - connInitLimit = 200 disableAutoUpdate = "disabled" ) @@ -208,7 +206,6 @@ type Engine struct { syncRespMux sync.RWMutex persistSyncResponse bool latestSyncResponse *mgmProto.SyncResponse - connSemaphore *semaphoregroup.SemaphoreGroup flowManager nftypes.FlowManager // auto-update @@ -224,6 +221,8 @@ type Engine struct { jobExecutor *jobexec.Executor jobExecutorWG sync.WaitGroup + + exposeManager *expose.Manager } // Peer is an instance of the Connection Peer @@ -266,7 +265,6 @@ func NewEngine( statusRecorder: statusRecorder, stateManager: stateManager, checks: checks, - connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL), jobExecutor: jobexec.NewExecutor(), } @@ -419,6 +417,7 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL) e.cancel() } e.ctx, e.cancel = context.WithCancel(e.clientCtx) + e.exposeManager = expose.NewManager(e.ctx, e.mgmClient) wgIface, err := e.newWgIface() if err != nil { @@ -801,7 +800,7 @@ func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdate disabled := autoUpdateSettings.Version == disableAutoUpdate - // Stop and cleanup if disabled + // stop and cleanup if disabled if e.updateManager != nil && disabled { log.Infof("auto-update is disabled, stopping update manager") e.updateManager.Stop() @@ -1539,7 +1538,6 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV IFaceDiscover: e.mobileDep.IFaceDiscover, RelayManager: e.relayManager, SrWatcher: e.srWatcher, - Semaphore: e.connSemaphore, } peerConn, err := peer.NewConn(config, serviceDependencies) if err != nil { @@ -1824,11 +1822,18 @@ func (e *Engine) GetRouteManager() routemanager.Manager { return e.routeManager } -// GetFirewallManager returns the firewall manager +// GetFirewallManager returns the firewall manager. func (e *Engine) GetFirewallManager() firewallManager.Manager { return e.firewall } +// GetExposeManager returns the expose session manager. +func (e *Engine) GetExposeManager() *expose.Manager { + e.syncMsgMux.Lock() + defer e.syncMsgMux.Unlock() + return e.exposeManager +} + func findIPFromInterfaceName(ifaceName string) (net.IP, error) { iface, err := net.InterfaceByName(ifaceName) if err != nil { diff --git a/client/internal/expose/manager.go b/client/internal/expose/manager.go new file mode 100644 index 000000000..8cd93685e --- /dev/null +++ b/client/internal/expose/manager.go @@ -0,0 +1,95 @@ +package expose + +import ( + "context" + "time" + + mgm "github.com/netbirdio/netbird/shared/management/client" + log "github.com/sirupsen/logrus" +) + +const renewTimeout = 10 * time.Second + +// Response holds the response from exposing a service. +type Response struct { + ServiceName string + ServiceURL string + Domain string +} + +type Request struct { + NamePrefix string + Domain string + Port uint16 + Protocol int + Pin string + Password string + UserGroups []string +} + +type ManagementClient interface { + CreateExpose(ctx context.Context, req mgm.ExposeRequest) (*mgm.ExposeResponse, error) + RenewExpose(ctx context.Context, domain string) error + StopExpose(ctx context.Context, domain string) error +} + +// Manager handles expose session lifecycle via the management client. +type Manager struct { + mgmClient ManagementClient + ctx context.Context +} + +// NewManager creates a new expose Manager using the given management client. +func NewManager(ctx context.Context, mgmClient ManagementClient) *Manager { + return &Manager{mgmClient: mgmClient, ctx: ctx} +} + +// Expose creates a new expose session via the management server. +func (m *Manager) Expose(ctx context.Context, req Request) (*Response, error) { + log.Infof("exposing service on port %d", req.Port) + resp, err := m.mgmClient.CreateExpose(ctx, toClientExposeRequest(req)) + if err != nil { + return nil, err + } + + log.Infof("expose session created for %s", resp.Domain) + + return fromClientExposeResponse(resp), nil +} + +func (m *Manager) KeepAlive(ctx context.Context, domain string) error { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + defer m.stop(domain) + + for { + select { + case <-ctx.Done(): + log.Infof("context canceled, stopping keep alive for %s", domain) + + return nil + case <-ticker.C: + if err := m.renew(ctx, domain); err != nil { + log.Errorf("renewing expose session for %s: %v", domain, err) + return err + } + } + } +} + +// renew extends the TTL of an active expose session. +func (m *Manager) renew(ctx context.Context, domain string) error { + renewCtx, cancel := context.WithTimeout(ctx, renewTimeout) + defer cancel() + return m.mgmClient.RenewExpose(renewCtx, domain) +} + +// stop terminates an active expose session. +func (m *Manager) stop(domain string) { + stopCtx, cancel := context.WithTimeout(m.ctx, renewTimeout) + defer cancel() + err := m.mgmClient.StopExpose(stopCtx, domain) + if err != nil { + log.Warnf("Failed stopping expose session for %s: %v", domain, err) + } +} diff --git a/client/internal/expose/manager_test.go b/client/internal/expose/manager_test.go new file mode 100644 index 000000000..87d43cdb0 --- /dev/null +++ b/client/internal/expose/manager_test.go @@ -0,0 +1,95 @@ +package expose + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + daemonProto "github.com/netbirdio/netbird/client/proto" + mgm "github.com/netbirdio/netbird/shared/management/client" +) + +func TestManager_Expose_Success(t *testing.T) { + mock := &mgm.MockClient{ + CreateExposeFunc: func(ctx context.Context, req mgm.ExposeRequest) (*mgm.ExposeResponse, error) { + return &mgm.ExposeResponse{ + ServiceName: "my-service", + ServiceURL: "https://my-service.example.com", + Domain: "my-service.example.com", + }, nil + }, + } + + m := NewManager(context.Background(), mock) + result, err := m.Expose(context.Background(), Request{Port: 8080}) + require.NoError(t, err) + assert.Equal(t, "my-service", result.ServiceName, "service name should match") + assert.Equal(t, "https://my-service.example.com", result.ServiceURL, "service URL should match") + assert.Equal(t, "my-service.example.com", result.Domain, "domain should match") +} + +func TestManager_Expose_Error(t *testing.T) { + mock := &mgm.MockClient{ + CreateExposeFunc: func(ctx context.Context, req mgm.ExposeRequest) (*mgm.ExposeResponse, error) { + return nil, errors.New("permission denied") + }, + } + + m := NewManager(context.Background(), mock) + _, err := m.Expose(context.Background(), Request{Port: 8080}) + require.Error(t, err) + assert.Contains(t, err.Error(), "permission denied", "error should propagate") +} + +func TestManager_Renew_Success(t *testing.T) { + mock := &mgm.MockClient{ + RenewExposeFunc: func(ctx context.Context, domain string) error { + assert.Equal(t, "my-service.example.com", domain, "domain should be passed through") + return nil + }, + } + + m := NewManager(context.Background(), mock) + err := m.renew(context.Background(), "my-service.example.com") + require.NoError(t, err) +} + +func TestManager_Renew_Timeout(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + mock := &mgm.MockClient{ + RenewExposeFunc: func(ctx context.Context, domain string) error { + return ctx.Err() + }, + } + + m := NewManager(ctx, mock) + err := m.renew(ctx, "my-service.example.com") + require.Error(t, err) +} + +func TestNewRequest(t *testing.T) { + req := &daemonProto.ExposeServiceRequest{ + Port: 8080, + Protocol: daemonProto.ExposeProtocol_EXPOSE_HTTPS, + Pin: "123456", + Password: "secret", + UserGroups: []string{"group1", "group2"}, + Domain: "custom.example.com", + NamePrefix: "my-prefix", + } + + exposeReq := NewRequest(req) + + assert.Equal(t, uint16(8080), exposeReq.Port, "port should match") + assert.Equal(t, int(daemonProto.ExposeProtocol_EXPOSE_HTTPS), exposeReq.Protocol, "protocol should match") + assert.Equal(t, "123456", exposeReq.Pin, "pin should match") + assert.Equal(t, "secret", exposeReq.Password, "password should match") + assert.Equal(t, []string{"group1", "group2"}, exposeReq.UserGroups, "user groups should match") + assert.Equal(t, "custom.example.com", exposeReq.Domain, "domain should match") + assert.Equal(t, "my-prefix", exposeReq.NamePrefix, "name prefix should match") +} diff --git a/client/internal/expose/request.go b/client/internal/expose/request.go new file mode 100644 index 000000000..7e12d0513 --- /dev/null +++ b/client/internal/expose/request.go @@ -0,0 +1,39 @@ +package expose + +import ( + daemonProto "github.com/netbirdio/netbird/client/proto" + mgm "github.com/netbirdio/netbird/shared/management/client" +) + +// NewRequest converts a daemon ExposeServiceRequest to a management ExposeServiceRequest. +func NewRequest(req *daemonProto.ExposeServiceRequest) *Request { + return &Request{ + Port: uint16(req.Port), + Protocol: int(req.Protocol), + Pin: req.Pin, + Password: req.Password, + UserGroups: req.UserGroups, + Domain: req.Domain, + NamePrefix: req.NamePrefix, + } +} + +func toClientExposeRequest(req Request) mgm.ExposeRequest { + return mgm.ExposeRequest{ + NamePrefix: req.NamePrefix, + Domain: req.Domain, + Port: req.Port, + Protocol: req.Protocol, + Pin: req.Pin, + Password: req.Password, + UserGroups: req.UserGroups, + } +} + +func fromClientExposeResponse(response *mgm.ExposeResponse) *Response { + return &Response{ + ServiceName: response.ServiceName, + Domain: response.Domain, + ServiceURL: response.ServiceURL, + } +} diff --git a/client/internal/networkmonitor/check_change_common.go b/client/internal/networkmonitor/check_change_common.go index c287236e8..a4a4f76ac 100644 --- a/client/internal/networkmonitor/check_change_common.go +++ b/client/internal/networkmonitor/check_change_common.go @@ -22,51 +22,56 @@ func prepareFd() (int, error) { func routeCheck(ctx context.Context, fd int, nexthopv4, nexthopv6 systemops.Nexthop) error { for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - buf := make([]byte, 2048) - n, err := unix.Read(fd, buf) + // Wait until fd is readable or context is cancelled, to avoid a busy-loop + // when the routing socket returns EAGAIN (e.g. immediately after wakeup). + if err := waitReadable(ctx, fd); err != nil { + return err + } + + buf := make([]byte, 2048) + n, err := unix.Read(fd, buf) + if err != nil { + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EINTR) { + continue + } + if errors.Is(err, unix.EBADF) || errors.Is(err, unix.EINVAL) { + return fmt.Errorf("routing socket closed: %w", err) + } + return fmt.Errorf("read routing socket: %w", err) + } + + if n < unix.SizeofRtMsghdr { + log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n) + continue + } + + msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0])) + + switch msg.Type { + // handle route changes + case unix.RTM_ADD, syscall.RTM_DELETE: + route, err := parseRouteMessage(buf[:n]) if err != nil { - if !errors.Is(err, unix.EBADF) && !errors.Is(err, unix.EINVAL) { - log.Warnf("Network monitor: failed to read from routing socket: %v", err) - } - continue - } - if n < unix.SizeofRtMsghdr { - log.Debugf("Network monitor: read from routing socket returned less than expected: %d bytes", n) + log.Debugf("Network monitor: error parsing routing message: %v", err) continue } - msg := (*unix.RtMsghdr)(unsafe.Pointer(&buf[0])) + if route.Dst.Bits() != 0 { + continue + } + intf := "" + if route.Interface != nil { + intf = route.Interface.Name + } switch msg.Type { - // handle route changes - case unix.RTM_ADD, syscall.RTM_DELETE: - route, err := parseRouteMessage(buf[:n]) - if err != nil { - log.Debugf("Network monitor: error parsing routing message: %v", err) - continue - } - - if route.Dst.Bits() != 0 { - continue - } - - intf := "" - if route.Interface != nil { - intf = route.Interface.Name - } - switch msg.Type { - case unix.RTM_ADD: - log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf) + case unix.RTM_ADD: + log.Infof("Network monitor: default route changed: via %s, interface %s", route.Gw, intf) + return nil + case unix.RTM_DELETE: + if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 { + log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf) return nil - case unix.RTM_DELETE: - if nexthopv4.Intf != nil && route.Gw.Compare(nexthopv4.IP) == 0 || nexthopv6.Intf != nil && route.Gw.Compare(nexthopv6.IP) == 0 { - log.Infof("Network monitor: default route removed: via %s, interface %s", route.Gw, intf) - return nil - } } } } @@ -90,3 +95,33 @@ func parseRouteMessage(buf []byte) (*systemops.Route, error) { return systemops.MsgToRoute(msg) } + +// waitReadable blocks until fd has data to read, or ctx is cancelled. +func waitReadable(ctx context.Context, fd int) error { + var fdset unix.FdSet + if fd < 0 || fd/unix.NFDBITS >= len(fdset.Bits) { + return fmt.Errorf("fd %d out of range for FdSet", fd) + } + + for { + if err := ctx.Err(); err != nil { + return err + } + + fdset = unix.FdSet{} + fdset.Set(fd) + // Use a 1-second timeout so we can re-check ctx periodically. + tv := unix.Timeval{Sec: 1} + n, err := unix.Select(fd+1, &fdset, nil, nil, &tv) + if err != nil { + if errors.Is(err, unix.EINTR) { + continue + } + return fmt.Errorf("select on routing socket: %w", err) + } + if n > 0 { + return nil + } + // timeout — loop back and re-check ctx + } +} diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 05a397f3d..b4f97016d 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -3,7 +3,6 @@ package peer import ( "context" "fmt" - "math/rand" "net" "net/netip" "runtime" @@ -25,7 +24,6 @@ import ( "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/route" relayClient "github.com/netbirdio/netbird/shared/relay/client" - semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" ) type ServiceDependencies struct { @@ -34,7 +32,6 @@ type ServiceDependencies struct { IFaceDiscover stdnet.ExternalIFaceDiscover RelayManager *relayClient.Manager SrWatcher *guard.SRWatcher - Semaphore *semaphoregroup.SemaphoreGroup PeerConnDispatcher *dispatcher.ConnectionDispatcher } @@ -111,9 +108,8 @@ type Conn struct { wgProxyRelay wgproxy.Proxy handshaker *Handshaker - guard *guard.Guard - semaphore *semaphoregroup.SemaphoreGroup - wg sync.WaitGroup + guard *guard.Guard + wg sync.WaitGroup // debug purpose dumpState *stateDump @@ -139,7 +135,6 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) { iFaceDiscover: services.IFaceDiscover, relayManager: services.RelayManager, srWatcher: services.SrWatcher, - semaphore: services.Semaphore, statusRelay: worker.NewAtomicStatus(), statusICE: worker.NewAtomicStatus(), dumpState: dumpState, @@ -154,15 +149,10 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) { // It will try to establish a connection using ICE and in parallel with relay. The higher priority connection type will // be used. func (conn *Conn) Open(engineCtx context.Context) error { - if err := conn.semaphore.Add(engineCtx); err != nil { - return err - } - conn.mu.Lock() defer conn.mu.Unlock() if conn.opened { - conn.semaphore.Done() return nil } @@ -173,7 +163,6 @@ func (conn *Conn) Open(engineCtx context.Context) error { relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally() workerICE, err := NewWorkerICE(conn.ctx, conn.Log, conn.config, conn, conn.signaler, conn.iFaceDiscover, conn.statusRecorder, relayIsSupportedLocally) if err != nil { - conn.semaphore.Done() return err } conn.workerICE = workerICE @@ -207,10 +196,6 @@ func (conn *Conn) Open(engineCtx context.Context) error { conn.wg.Add(1) go func() { defer conn.wg.Done() - - conn.waitInitialRandomSleepTime(conn.ctx) - conn.semaphore.Done() - conn.guard.Start(conn.ctx, conn.onGuardEvent) }() conn.opened = true @@ -670,19 +655,6 @@ func (conn *Conn) doOnConnected(remoteRosenpassPubKey []byte, remoteRosenpassAdd } } -func (conn *Conn) waitInitialRandomSleepTime(ctx context.Context) { - maxWait := 300 - duration := time.Duration(rand.Intn(maxWait)) * time.Millisecond - - timeout := time.NewTimer(duration) - defer timeout.Stop() - - select { - case <-ctx.Done(): - case <-timeout.C: - } -} - func (conn *Conn) isRelayed() bool { switch conn.currentConnPriority { case conntype.Relay, conntype.ICETurn: diff --git a/client/internal/peer/conn_test.go b/client/internal/peer/conn_test.go index 32383b530..59216b647 100644 --- a/client/internal/peer/conn_test.go +++ b/client/internal/peer/conn_test.go @@ -15,7 +15,6 @@ import ( "github.com/netbirdio/netbird/client/internal/peer/ice" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/util" - semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group" ) var testDispatcher = dispatcher.NewConnectionDispatcher() @@ -53,7 +52,6 @@ func TestConn_GetKey(t *testing.T) { sd := ServiceDependencies{ SrWatcher: swWatcher, - Semaphore: semaphoregroup.NewSemaphoreGroup(1), PeerConnDispatcher: testDispatcher, } conn, err := NewConn(connConf, sd) @@ -71,7 +69,6 @@ func TestConn_OnRemoteOffer(t *testing.T) { sd := ServiceDependencies{ StatusRecorder: NewRecorder("https://mgm"), SrWatcher: swWatcher, - Semaphore: semaphoregroup.NewSemaphoreGroup(1), PeerConnDispatcher: testDispatcher, } conn, err := NewConn(connConf, sd) @@ -110,7 +107,6 @@ func TestConn_OnRemoteAnswer(t *testing.T) { sd := ServiceDependencies{ StatusRecorder: NewRecorder("https://mgm"), SrWatcher: swWatcher, - Semaphore: semaphoregroup.NewSemaphoreGroup(1), PeerConnDispatcher: testDispatcher, } conn, err := NewConn(connConf, sd) diff --git a/client/internal/sleep/handler/handler.go b/client/internal/sleep/handler/handler.go new file mode 100644 index 000000000..9c2c5d4d5 --- /dev/null +++ b/client/internal/sleep/handler/handler.go @@ -0,0 +1,80 @@ +package handler + +import ( + "context" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal" +) + +type Agent interface { + Up(ctx context.Context) error + Down(ctx context.Context) error + Status() (internal.StatusType, error) +} + +type SleepHandler struct { + agent Agent + + mu sync.Mutex + // sleepTriggeredDown indicates whether the sleep handler triggered the last client down, to avoid unnecessary up on wake + sleepTriggeredDown bool +} + +func New(agent Agent) *SleepHandler { + return &SleepHandler{ + agent: agent, + } +} + +func (s *SleepHandler) HandleWakeUp(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.sleepTriggeredDown { + log.Info("skipping up because wasn't sleep down") + return nil + } + + // avoid other wakeup runs if sleep didn't make the computer sleep + s.sleepTriggeredDown = false + + log.Info("running up after wake up") + err := s.agent.Up(ctx) + if err != nil { + log.Errorf("running up failed: %v", err) + return err + } + + log.Info("running up command executed successfully") + return nil +} + +func (s *SleepHandler) HandleSleep(ctx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + status, err := s.agent.Status() + if err != nil { + return err + } + + if status != internal.StatusConnecting && status != internal.StatusConnected { + log.Infof("skipping setting the agent down because status is %s", status) + return nil + } + + log.Info("running down after system started sleeping") + + if err = s.agent.Down(ctx); err != nil { + log.Errorf("running down failed: %v", err) + return err + } + + s.sleepTriggeredDown = true + + log.Info("running down executed successfully") + return nil +} diff --git a/client/internal/sleep/handler/handler_test.go b/client/internal/sleep/handler/handler_test.go new file mode 100644 index 000000000..9f79428fb --- /dev/null +++ b/client/internal/sleep/handler/handler_test.go @@ -0,0 +1,153 @@ +package handler + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/internal" +) + +type mockAgent struct { + upErr error + downErr error + statusErr error + status internal.StatusType + upCalls int +} + +func (m *mockAgent) Up(_ context.Context) error { + m.upCalls++ + return m.upErr +} + +func (m *mockAgent) Down(_ context.Context) error { + return m.downErr +} + +func (m *mockAgent) Status() (internal.StatusType, error) { + return m.status, m.statusErr +} + +func newHandler(status internal.StatusType) (*SleepHandler, *mockAgent) { + agent := &mockAgent{status: status} + return New(agent), agent +} + +func TestHandleWakeUp_SkipsWhenFlagFalse(t *testing.T) { + h, agent := newHandler(internal.StatusIdle) + + err := h.HandleWakeUp(context.Background()) + + require.NoError(t, err) + assert.Equal(t, 0, agent.upCalls, "Up should not be called when flag is false") +} + +func TestHandleWakeUp_ResetsFlagBeforeUp(t *testing.T) { + h, _ := newHandler(internal.StatusIdle) + h.sleepTriggeredDown = true + + // Even if Up fails, flag should be reset + _ = h.HandleWakeUp(context.Background()) + + assert.False(t, h.sleepTriggeredDown, "flag must be reset before calling Up") +} + +func TestHandleWakeUp_CallsUpWhenFlagSet(t *testing.T) { + h, agent := newHandler(internal.StatusIdle) + h.sleepTriggeredDown = true + + err := h.HandleWakeUp(context.Background()) + + require.NoError(t, err) + assert.Equal(t, 1, agent.upCalls) + assert.False(t, h.sleepTriggeredDown) +} + +func TestHandleWakeUp_ReturnsErrorFromUp(t *testing.T) { + h, agent := newHandler(internal.StatusIdle) + h.sleepTriggeredDown = true + agent.upErr = errors.New("up failed") + + err := h.HandleWakeUp(context.Background()) + + assert.ErrorIs(t, err, agent.upErr) + assert.False(t, h.sleepTriggeredDown, "flag should still be reset even when Up fails") +} + +func TestHandleWakeUp_SecondCallIsNoOp(t *testing.T) { + h, agent := newHandler(internal.StatusIdle) + h.sleepTriggeredDown = true + + _ = h.HandleWakeUp(context.Background()) + err := h.HandleWakeUp(context.Background()) + + require.NoError(t, err) + assert.Equal(t, 1, agent.upCalls, "second wakeup should be no-op") +} + +func TestHandleSleep_SkipsForNonActiveStates(t *testing.T) { + tests := []struct { + name string + status internal.StatusType + }{ + {"Idle", internal.StatusIdle}, + {"NeedsLogin", internal.StatusNeedsLogin}, + {"LoginFailed", internal.StatusLoginFailed}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h, _ := newHandler(tt.status) + + err := h.HandleSleep(context.Background()) + + require.NoError(t, err) + assert.False(t, h.sleepTriggeredDown) + }) + } +} + +func TestHandleSleep_ProceedsForActiveStates(t *testing.T) { + tests := []struct { + name string + status internal.StatusType + }{ + {"Connecting", internal.StatusConnecting}, + {"Connected", internal.StatusConnected}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h, _ := newHandler(tt.status) + + err := h.HandleSleep(context.Background()) + + require.NoError(t, err) + assert.True(t, h.sleepTriggeredDown) + }) + } +} + +func TestHandleSleep_ReturnsErrorFromStatus(t *testing.T) { + agent := &mockAgent{statusErr: errors.New("status error")} + h := New(agent) + + err := h.HandleSleep(context.Background()) + + assert.ErrorIs(t, err, agent.statusErr) + assert.False(t, h.sleepTriggeredDown) +} + +func TestHandleSleep_ReturnsErrorFromDown(t *testing.T) { + agent := &mockAgent{status: internal.StatusConnected, downErr: errors.New("down failed")} + h := New(agent) + + err := h.HandleSleep(context.Background()) + + assert.ErrorIs(t, err, agent.downErr) + assert.False(t, h.sleepTriggeredDown, "flag should not be set when Down fails") +} diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 1d9d7233c..3879beba3 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 -// protoc v6.32.1 +// protoc v6.33.3 // source: daemon.proto package proto @@ -88,6 +88,58 @@ func (LogLevel) EnumDescriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{0} } +type ExposeProtocol int32 + +const ( + ExposeProtocol_EXPOSE_HTTP ExposeProtocol = 0 + ExposeProtocol_EXPOSE_HTTPS ExposeProtocol = 1 + ExposeProtocol_EXPOSE_TCP ExposeProtocol = 2 + ExposeProtocol_EXPOSE_UDP ExposeProtocol = 3 +) + +// Enum value maps for ExposeProtocol. +var ( + ExposeProtocol_name = map[int32]string{ + 0: "EXPOSE_HTTP", + 1: "EXPOSE_HTTPS", + 2: "EXPOSE_TCP", + 3: "EXPOSE_UDP", + } + ExposeProtocol_value = map[string]int32{ + "EXPOSE_HTTP": 0, + "EXPOSE_HTTPS": 1, + "EXPOSE_TCP": 2, + "EXPOSE_UDP": 3, + } +) + +func (x ExposeProtocol) Enum() *ExposeProtocol { + p := new(ExposeProtocol) + *p = x + return p +} + +func (x ExposeProtocol) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExposeProtocol) Descriptor() protoreflect.EnumDescriptor { + return file_daemon_proto_enumTypes[1].Descriptor() +} + +func (ExposeProtocol) Type() protoreflect.EnumType { + return &file_daemon_proto_enumTypes[1] +} + +func (x ExposeProtocol) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExposeProtocol.Descriptor instead. +func (ExposeProtocol) EnumDescriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{1} +} + // avoid collision with loglevel enum type OSLifecycleRequest_CycleType int32 @@ -122,11 +174,11 @@ func (x OSLifecycleRequest_CycleType) String() string { } func (OSLifecycleRequest_CycleType) Descriptor() protoreflect.EnumDescriptor { - return file_daemon_proto_enumTypes[1].Descriptor() + return file_daemon_proto_enumTypes[2].Descriptor() } func (OSLifecycleRequest_CycleType) Type() protoreflect.EnumType { - return &file_daemon_proto_enumTypes[1] + return &file_daemon_proto_enumTypes[2] } func (x OSLifecycleRequest_CycleType) Number() protoreflect.EnumNumber { @@ -174,11 +226,11 @@ func (x SystemEvent_Severity) String() string { } func (SystemEvent_Severity) Descriptor() protoreflect.EnumDescriptor { - return file_daemon_proto_enumTypes[2].Descriptor() + return file_daemon_proto_enumTypes[3].Descriptor() } func (SystemEvent_Severity) Type() protoreflect.EnumType { - return &file_daemon_proto_enumTypes[2] + return &file_daemon_proto_enumTypes[3] } func (x SystemEvent_Severity) Number() protoreflect.EnumNumber { @@ -229,11 +281,11 @@ func (x SystemEvent_Category) String() string { } func (SystemEvent_Category) Descriptor() protoreflect.EnumDescriptor { - return file_daemon_proto_enumTypes[3].Descriptor() + return file_daemon_proto_enumTypes[4].Descriptor() } func (SystemEvent_Category) Type() protoreflect.EnumType { - return &file_daemon_proto_enumTypes[3] + return &file_daemon_proto_enumTypes[4] } func (x SystemEvent_Category) Number() protoreflect.EnumNumber { @@ -5600,6 +5652,224 @@ func (x *InstallerResultResponse) GetErrorMsg() string { return "" } +type ExposeServiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Port uint32 `protobuf:"varint,1,opt,name=port,proto3" json:"port,omitempty"` + Protocol ExposeProtocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=daemon.ExposeProtocol" json:"protocol,omitempty"` + Pin string `protobuf:"bytes,3,opt,name=pin,proto3" json:"pin,omitempty"` + Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"` + UserGroups []string `protobuf:"bytes,5,rep,name=user_groups,json=userGroups,proto3" json:"user_groups,omitempty"` + Domain string `protobuf:"bytes,6,opt,name=domain,proto3" json:"domain,omitempty"` + NamePrefix string `protobuf:"bytes,7,opt,name=name_prefix,json=namePrefix,proto3" json:"name_prefix,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExposeServiceRequest) Reset() { + *x = ExposeServiceRequest{} + mi := &file_daemon_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExposeServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposeServiceRequest) ProtoMessage() {} + +func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[85] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. +func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{85} +} + +func (x *ExposeServiceRequest) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ExposeServiceRequest) GetProtocol() ExposeProtocol { + if x != nil { + return x.Protocol + } + return ExposeProtocol_EXPOSE_HTTP +} + +func (x *ExposeServiceRequest) GetPin() string { + if x != nil { + return x.Pin + } + return "" +} + +func (x *ExposeServiceRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *ExposeServiceRequest) GetUserGroups() []string { + if x != nil { + return x.UserGroups + } + return nil +} + +func (x *ExposeServiceRequest) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *ExposeServiceRequest) GetNamePrefix() string { + if x != nil { + return x.NamePrefix + } + return "" +} + +type ExposeServiceEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *ExposeServiceEvent_Ready + Event isExposeServiceEvent_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExposeServiceEvent) Reset() { + *x = ExposeServiceEvent{} + mi := &file_daemon_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExposeServiceEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposeServiceEvent) ProtoMessage() {} + +func (x *ExposeServiceEvent) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[86] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExposeServiceEvent.ProtoReflect.Descriptor instead. +func (*ExposeServiceEvent) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{86} +} + +func (x *ExposeServiceEvent) GetEvent() isExposeServiceEvent_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *ExposeServiceEvent) GetReady() *ExposeServiceReady { + if x != nil { + if x, ok := x.Event.(*ExposeServiceEvent_Ready); ok { + return x.Ready + } + } + return nil +} + +type isExposeServiceEvent_Event interface { + isExposeServiceEvent_Event() +} + +type ExposeServiceEvent_Ready struct { + Ready *ExposeServiceReady `protobuf:"bytes,1,opt,name=ready,proto3,oneof"` +} + +func (*ExposeServiceEvent_Ready) isExposeServiceEvent_Event() {} + +type ExposeServiceReady struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + ServiceUrl string `protobuf:"bytes,2,opt,name=service_url,json=serviceUrl,proto3" json:"service_url,omitempty"` + Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExposeServiceReady) Reset() { + *x = ExposeServiceReady{} + mi := &file_daemon_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExposeServiceReady) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposeServiceReady) ProtoMessage() {} + +func (x *ExposeServiceReady) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[87] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExposeServiceReady.ProtoReflect.Descriptor instead. +func (*ExposeServiceReady) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{87} +} + +func (x *ExposeServiceReady) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *ExposeServiceReady) GetServiceUrl() string { + if x != nil { + return x.ServiceUrl + } + return "" +} + +func (x *ExposeServiceReady) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + type PortInfo_Range struct { state protoimpl.MessageState `protogen:"open.v1"` Start uint32 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` @@ -5610,7 +5880,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5622,7 +5892,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[86] + mi := &file_daemon_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6149,7 +6419,25 @@ const file_daemon_proto_rawDesc = "" + "\x16InstallerResultRequest\"O\n" + "\x17InstallerResultResponse\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" + - "\berrorMsg\x18\x02 \x01(\tR\berrorMsg*b\n" + + "\berrorMsg\x18\x02 \x01(\tR\berrorMsg\"\xe6\x01\n" + + "\x14ExposeServiceRequest\x12\x12\n" + + "\x04port\x18\x01 \x01(\rR\x04port\x122\n" + + "\bprotocol\x18\x02 \x01(\x0e2\x16.daemon.ExposeProtocolR\bprotocol\x12\x10\n" + + "\x03pin\x18\x03 \x01(\tR\x03pin\x12\x1a\n" + + "\bpassword\x18\x04 \x01(\tR\bpassword\x12\x1f\n" + + "\vuser_groups\x18\x05 \x03(\tR\n" + + "userGroups\x12\x16\n" + + "\x06domain\x18\x06 \x01(\tR\x06domain\x12\x1f\n" + + "\vname_prefix\x18\a \x01(\tR\n" + + "namePrefix\"Q\n" + + "\x12ExposeServiceEvent\x122\n" + + "\x05ready\x18\x01 \x01(\v2\x1a.daemon.ExposeServiceReadyH\x00R\x05readyB\a\n" + + "\x05event\"p\n" + + "\x12ExposeServiceReady\x12!\n" + + "\fservice_name\x18\x01 \x01(\tR\vserviceName\x12\x1f\n" + + "\vservice_url\x18\x02 \x01(\tR\n" + + "serviceUrl\x12\x16\n" + + "\x06domain\x18\x03 \x01(\tR\x06domain*b\n" + "\bLogLevel\x12\v\n" + "\aUNKNOWN\x10\x00\x12\t\n" + "\x05PANIC\x10\x01\x12\t\n" + @@ -6158,7 +6446,14 @@ const file_daemon_proto_rawDesc = "" + "\x04WARN\x10\x04\x12\b\n" + "\x04INFO\x10\x05\x12\t\n" + "\x05DEBUG\x10\x06\x12\t\n" + - "\x05TRACE\x10\a2\xdd\x14\n" + + "\x05TRACE\x10\a*S\n" + + "\x0eExposeProtocol\x12\x0f\n" + + "\vEXPOSE_HTTP\x10\x00\x12\x10\n" + + "\fEXPOSE_HTTPS\x10\x01\x12\x0e\n" + + "\n" + + "EXPOSE_TCP\x10\x02\x12\x0e\n" + + "\n" + + "EXPOSE_UDP\x10\x032\xac\x15\n" + "\rDaemonService\x126\n" + "\x05Login\x12\x14.daemon.LoginRequest\x1a\x15.daemon.LoginResponse\"\x00\x12K\n" + "\fWaitSSOLogin\x12\x1b.daemon.WaitSSOLoginRequest\x1a\x1c.daemon.WaitSSOLoginResponse\"\x00\x12-\n" + @@ -6197,7 +6492,8 @@ const file_daemon_proto_rawDesc = "" + "\x0fStartCPUProfile\x12\x1e.daemon.StartCPUProfileRequest\x1a\x1f.daemon.StartCPUProfileResponse\"\x00\x12Q\n" + "\x0eStopCPUProfile\x12\x1d.daemon.StopCPUProfileRequest\x1a\x1e.daemon.StopCPUProfileResponse\"\x00\x12N\n" + "\x11NotifyOSLifecycle\x12\x1a.daemon.OSLifecycleRequest\x1a\x1b.daemon.OSLifecycleResponse\"\x00\x12W\n" + - "\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00B\bZ\x06/protob\x06proto3" + "\x12GetInstallerResult\x12\x1e.daemon.InstallerResultRequest\x1a\x1f.daemon.InstallerResultResponse\"\x00\x12M\n" + + "\rExposeService\x12\x1c.daemon.ExposeServiceRequest\x1a\x1a.daemon.ExposeServiceEvent\"\x000\x01B\bZ\x06/protob\x06proto3" var ( file_daemon_proto_rawDescOnce sync.Once @@ -6211,214 +6507,222 @@ func file_daemon_proto_rawDescGZIP() []byte { return file_daemon_proto_rawDescData } -var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 88) +var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 91) var file_daemon_proto_goTypes = []any{ (LogLevel)(0), // 0: daemon.LogLevel - (OSLifecycleRequest_CycleType)(0), // 1: daemon.OSLifecycleRequest.CycleType - (SystemEvent_Severity)(0), // 2: daemon.SystemEvent.Severity - (SystemEvent_Category)(0), // 3: daemon.SystemEvent.Category - (*EmptyRequest)(nil), // 4: daemon.EmptyRequest - (*OSLifecycleRequest)(nil), // 5: daemon.OSLifecycleRequest - (*OSLifecycleResponse)(nil), // 6: daemon.OSLifecycleResponse - (*LoginRequest)(nil), // 7: daemon.LoginRequest - (*LoginResponse)(nil), // 8: daemon.LoginResponse - (*WaitSSOLoginRequest)(nil), // 9: daemon.WaitSSOLoginRequest - (*WaitSSOLoginResponse)(nil), // 10: daemon.WaitSSOLoginResponse - (*UpRequest)(nil), // 11: daemon.UpRequest - (*UpResponse)(nil), // 12: daemon.UpResponse - (*StatusRequest)(nil), // 13: daemon.StatusRequest - (*StatusResponse)(nil), // 14: daemon.StatusResponse - (*DownRequest)(nil), // 15: daemon.DownRequest - (*DownResponse)(nil), // 16: daemon.DownResponse - (*GetConfigRequest)(nil), // 17: daemon.GetConfigRequest - (*GetConfigResponse)(nil), // 18: daemon.GetConfigResponse - (*PeerState)(nil), // 19: daemon.PeerState - (*LocalPeerState)(nil), // 20: daemon.LocalPeerState - (*SignalState)(nil), // 21: daemon.SignalState - (*ManagementState)(nil), // 22: daemon.ManagementState - (*RelayState)(nil), // 23: daemon.RelayState - (*NSGroupState)(nil), // 24: daemon.NSGroupState - (*SSHSessionInfo)(nil), // 25: daemon.SSHSessionInfo - (*SSHServerState)(nil), // 26: daemon.SSHServerState - (*FullStatus)(nil), // 27: daemon.FullStatus - (*ListNetworksRequest)(nil), // 28: daemon.ListNetworksRequest - (*ListNetworksResponse)(nil), // 29: daemon.ListNetworksResponse - (*SelectNetworksRequest)(nil), // 30: daemon.SelectNetworksRequest - (*SelectNetworksResponse)(nil), // 31: daemon.SelectNetworksResponse - (*IPList)(nil), // 32: daemon.IPList - (*Network)(nil), // 33: daemon.Network - (*PortInfo)(nil), // 34: daemon.PortInfo - (*ForwardingRule)(nil), // 35: daemon.ForwardingRule - (*ForwardingRulesResponse)(nil), // 36: daemon.ForwardingRulesResponse - (*DebugBundleRequest)(nil), // 37: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 38: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 39: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 40: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 41: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 42: daemon.SetLogLevelResponse - (*State)(nil), // 43: daemon.State - (*ListStatesRequest)(nil), // 44: daemon.ListStatesRequest - (*ListStatesResponse)(nil), // 45: daemon.ListStatesResponse - (*CleanStateRequest)(nil), // 46: daemon.CleanStateRequest - (*CleanStateResponse)(nil), // 47: daemon.CleanStateResponse - (*DeleteStateRequest)(nil), // 48: daemon.DeleteStateRequest - (*DeleteStateResponse)(nil), // 49: daemon.DeleteStateResponse - (*SetSyncResponsePersistenceRequest)(nil), // 50: daemon.SetSyncResponsePersistenceRequest - (*SetSyncResponsePersistenceResponse)(nil), // 51: daemon.SetSyncResponsePersistenceResponse - (*TCPFlags)(nil), // 52: daemon.TCPFlags - (*TracePacketRequest)(nil), // 53: daemon.TracePacketRequest - (*TraceStage)(nil), // 54: daemon.TraceStage - (*TracePacketResponse)(nil), // 55: daemon.TracePacketResponse - (*SubscribeRequest)(nil), // 56: daemon.SubscribeRequest - (*SystemEvent)(nil), // 57: daemon.SystemEvent - (*GetEventsRequest)(nil), // 58: daemon.GetEventsRequest - (*GetEventsResponse)(nil), // 59: daemon.GetEventsResponse - (*SwitchProfileRequest)(nil), // 60: daemon.SwitchProfileRequest - (*SwitchProfileResponse)(nil), // 61: daemon.SwitchProfileResponse - (*SetConfigRequest)(nil), // 62: daemon.SetConfigRequest - (*SetConfigResponse)(nil), // 63: daemon.SetConfigResponse - (*AddProfileRequest)(nil), // 64: daemon.AddProfileRequest - (*AddProfileResponse)(nil), // 65: daemon.AddProfileResponse - (*RemoveProfileRequest)(nil), // 66: daemon.RemoveProfileRequest - (*RemoveProfileResponse)(nil), // 67: daemon.RemoveProfileResponse - (*ListProfilesRequest)(nil), // 68: daemon.ListProfilesRequest - (*ListProfilesResponse)(nil), // 69: daemon.ListProfilesResponse - (*Profile)(nil), // 70: daemon.Profile - (*GetActiveProfileRequest)(nil), // 71: daemon.GetActiveProfileRequest - (*GetActiveProfileResponse)(nil), // 72: daemon.GetActiveProfileResponse - (*LogoutRequest)(nil), // 73: daemon.LogoutRequest - (*LogoutResponse)(nil), // 74: daemon.LogoutResponse - (*GetFeaturesRequest)(nil), // 75: daemon.GetFeaturesRequest - (*GetFeaturesResponse)(nil), // 76: daemon.GetFeaturesResponse - (*GetPeerSSHHostKeyRequest)(nil), // 77: daemon.GetPeerSSHHostKeyRequest - (*GetPeerSSHHostKeyResponse)(nil), // 78: daemon.GetPeerSSHHostKeyResponse - (*RequestJWTAuthRequest)(nil), // 79: daemon.RequestJWTAuthRequest - (*RequestJWTAuthResponse)(nil), // 80: daemon.RequestJWTAuthResponse - (*WaitJWTTokenRequest)(nil), // 81: daemon.WaitJWTTokenRequest - (*WaitJWTTokenResponse)(nil), // 82: daemon.WaitJWTTokenResponse - (*StartCPUProfileRequest)(nil), // 83: daemon.StartCPUProfileRequest - (*StartCPUProfileResponse)(nil), // 84: daemon.StartCPUProfileResponse - (*StopCPUProfileRequest)(nil), // 85: daemon.StopCPUProfileRequest - (*StopCPUProfileResponse)(nil), // 86: daemon.StopCPUProfileResponse - (*InstallerResultRequest)(nil), // 87: daemon.InstallerResultRequest - (*InstallerResultResponse)(nil), // 88: daemon.InstallerResultResponse - nil, // 89: daemon.Network.ResolvedIPsEntry - (*PortInfo_Range)(nil), // 90: daemon.PortInfo.Range - nil, // 91: daemon.SystemEvent.MetadataEntry - (*durationpb.Duration)(nil), // 92: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 93: google.protobuf.Timestamp + (ExposeProtocol)(0), // 1: daemon.ExposeProtocol + (OSLifecycleRequest_CycleType)(0), // 2: daemon.OSLifecycleRequest.CycleType + (SystemEvent_Severity)(0), // 3: daemon.SystemEvent.Severity + (SystemEvent_Category)(0), // 4: daemon.SystemEvent.Category + (*EmptyRequest)(nil), // 5: daemon.EmptyRequest + (*OSLifecycleRequest)(nil), // 6: daemon.OSLifecycleRequest + (*OSLifecycleResponse)(nil), // 7: daemon.OSLifecycleResponse + (*LoginRequest)(nil), // 8: daemon.LoginRequest + (*LoginResponse)(nil), // 9: daemon.LoginResponse + (*WaitSSOLoginRequest)(nil), // 10: daemon.WaitSSOLoginRequest + (*WaitSSOLoginResponse)(nil), // 11: daemon.WaitSSOLoginResponse + (*UpRequest)(nil), // 12: daemon.UpRequest + (*UpResponse)(nil), // 13: daemon.UpResponse + (*StatusRequest)(nil), // 14: daemon.StatusRequest + (*StatusResponse)(nil), // 15: daemon.StatusResponse + (*DownRequest)(nil), // 16: daemon.DownRequest + (*DownResponse)(nil), // 17: daemon.DownResponse + (*GetConfigRequest)(nil), // 18: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 19: daemon.GetConfigResponse + (*PeerState)(nil), // 20: daemon.PeerState + (*LocalPeerState)(nil), // 21: daemon.LocalPeerState + (*SignalState)(nil), // 22: daemon.SignalState + (*ManagementState)(nil), // 23: daemon.ManagementState + (*RelayState)(nil), // 24: daemon.RelayState + (*NSGroupState)(nil), // 25: daemon.NSGroupState + (*SSHSessionInfo)(nil), // 26: daemon.SSHSessionInfo + (*SSHServerState)(nil), // 27: daemon.SSHServerState + (*FullStatus)(nil), // 28: daemon.FullStatus + (*ListNetworksRequest)(nil), // 29: daemon.ListNetworksRequest + (*ListNetworksResponse)(nil), // 30: daemon.ListNetworksResponse + (*SelectNetworksRequest)(nil), // 31: daemon.SelectNetworksRequest + (*SelectNetworksResponse)(nil), // 32: daemon.SelectNetworksResponse + (*IPList)(nil), // 33: daemon.IPList + (*Network)(nil), // 34: daemon.Network + (*PortInfo)(nil), // 35: daemon.PortInfo + (*ForwardingRule)(nil), // 36: daemon.ForwardingRule + (*ForwardingRulesResponse)(nil), // 37: daemon.ForwardingRulesResponse + (*DebugBundleRequest)(nil), // 38: daemon.DebugBundleRequest + (*DebugBundleResponse)(nil), // 39: daemon.DebugBundleResponse + (*GetLogLevelRequest)(nil), // 40: daemon.GetLogLevelRequest + (*GetLogLevelResponse)(nil), // 41: daemon.GetLogLevelResponse + (*SetLogLevelRequest)(nil), // 42: daemon.SetLogLevelRequest + (*SetLogLevelResponse)(nil), // 43: daemon.SetLogLevelResponse + (*State)(nil), // 44: daemon.State + (*ListStatesRequest)(nil), // 45: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 46: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 47: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 48: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 49: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 50: daemon.DeleteStateResponse + (*SetSyncResponsePersistenceRequest)(nil), // 51: daemon.SetSyncResponsePersistenceRequest + (*SetSyncResponsePersistenceResponse)(nil), // 52: daemon.SetSyncResponsePersistenceResponse + (*TCPFlags)(nil), // 53: daemon.TCPFlags + (*TracePacketRequest)(nil), // 54: daemon.TracePacketRequest + (*TraceStage)(nil), // 55: daemon.TraceStage + (*TracePacketResponse)(nil), // 56: daemon.TracePacketResponse + (*SubscribeRequest)(nil), // 57: daemon.SubscribeRequest + (*SystemEvent)(nil), // 58: daemon.SystemEvent + (*GetEventsRequest)(nil), // 59: daemon.GetEventsRequest + (*GetEventsResponse)(nil), // 60: daemon.GetEventsResponse + (*SwitchProfileRequest)(nil), // 61: daemon.SwitchProfileRequest + (*SwitchProfileResponse)(nil), // 62: daemon.SwitchProfileResponse + (*SetConfigRequest)(nil), // 63: daemon.SetConfigRequest + (*SetConfigResponse)(nil), // 64: daemon.SetConfigResponse + (*AddProfileRequest)(nil), // 65: daemon.AddProfileRequest + (*AddProfileResponse)(nil), // 66: daemon.AddProfileResponse + (*RemoveProfileRequest)(nil), // 67: daemon.RemoveProfileRequest + (*RemoveProfileResponse)(nil), // 68: daemon.RemoveProfileResponse + (*ListProfilesRequest)(nil), // 69: daemon.ListProfilesRequest + (*ListProfilesResponse)(nil), // 70: daemon.ListProfilesResponse + (*Profile)(nil), // 71: daemon.Profile + (*GetActiveProfileRequest)(nil), // 72: daemon.GetActiveProfileRequest + (*GetActiveProfileResponse)(nil), // 73: daemon.GetActiveProfileResponse + (*LogoutRequest)(nil), // 74: daemon.LogoutRequest + (*LogoutResponse)(nil), // 75: daemon.LogoutResponse + (*GetFeaturesRequest)(nil), // 76: daemon.GetFeaturesRequest + (*GetFeaturesResponse)(nil), // 77: daemon.GetFeaturesResponse + (*GetPeerSSHHostKeyRequest)(nil), // 78: daemon.GetPeerSSHHostKeyRequest + (*GetPeerSSHHostKeyResponse)(nil), // 79: daemon.GetPeerSSHHostKeyResponse + (*RequestJWTAuthRequest)(nil), // 80: daemon.RequestJWTAuthRequest + (*RequestJWTAuthResponse)(nil), // 81: daemon.RequestJWTAuthResponse + (*WaitJWTTokenRequest)(nil), // 82: daemon.WaitJWTTokenRequest + (*WaitJWTTokenResponse)(nil), // 83: daemon.WaitJWTTokenResponse + (*StartCPUProfileRequest)(nil), // 84: daemon.StartCPUProfileRequest + (*StartCPUProfileResponse)(nil), // 85: daemon.StartCPUProfileResponse + (*StopCPUProfileRequest)(nil), // 86: daemon.StopCPUProfileRequest + (*StopCPUProfileResponse)(nil), // 87: daemon.StopCPUProfileResponse + (*InstallerResultRequest)(nil), // 88: daemon.InstallerResultRequest + (*InstallerResultResponse)(nil), // 89: daemon.InstallerResultResponse + (*ExposeServiceRequest)(nil), // 90: daemon.ExposeServiceRequest + (*ExposeServiceEvent)(nil), // 91: daemon.ExposeServiceEvent + (*ExposeServiceReady)(nil), // 92: daemon.ExposeServiceReady + nil, // 93: daemon.Network.ResolvedIPsEntry + (*PortInfo_Range)(nil), // 94: daemon.PortInfo.Range + nil, // 95: daemon.SystemEvent.MetadataEntry + (*durationpb.Duration)(nil), // 96: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 97: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 1, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType - 92, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 27, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 93, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 93, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 92, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration - 25, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo - 22, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState - 21, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState - 20, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState - 19, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState - 23, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState - 24, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState - 57, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent - 26, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState - 33, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network - 89, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry - 90, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range - 34, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo - 34, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo - 35, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule + 2, // 0: daemon.OSLifecycleRequest.type:type_name -> daemon.OSLifecycleRequest.CycleType + 96, // 1: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 28, // 2: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus + 97, // 3: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 97, // 4: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 96, // 5: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 26, // 6: daemon.SSHServerState.sessions:type_name -> daemon.SSHSessionInfo + 23, // 7: daemon.FullStatus.managementState:type_name -> daemon.ManagementState + 22, // 8: daemon.FullStatus.signalState:type_name -> daemon.SignalState + 21, // 9: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState + 20, // 10: daemon.FullStatus.peers:type_name -> daemon.PeerState + 24, // 11: daemon.FullStatus.relays:type_name -> daemon.RelayState + 25, // 12: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState + 58, // 13: daemon.FullStatus.events:type_name -> daemon.SystemEvent + 27, // 14: daemon.FullStatus.sshServerState:type_name -> daemon.SSHServerState + 34, // 15: daemon.ListNetworksResponse.routes:type_name -> daemon.Network + 93, // 16: daemon.Network.resolvedIPs:type_name -> daemon.Network.ResolvedIPsEntry + 94, // 17: daemon.PortInfo.range:type_name -> daemon.PortInfo.Range + 35, // 18: daemon.ForwardingRule.destinationPort:type_name -> daemon.PortInfo + 35, // 19: daemon.ForwardingRule.translatedPort:type_name -> daemon.PortInfo + 36, // 20: daemon.ForwardingRulesResponse.rules:type_name -> daemon.ForwardingRule 0, // 21: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel 0, // 22: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 43, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State - 52, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags - 54, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage - 2, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity - 3, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category - 93, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp - 91, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry - 57, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent - 92, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration - 70, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile - 32, // 33: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList - 7, // 34: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 9, // 35: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 11, // 36: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 13, // 37: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 15, // 38: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 17, // 39: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 28, // 40: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest - 30, // 41: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest - 30, // 42: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest - 4, // 43: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest - 37, // 44: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 39, // 45: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 41, // 46: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 44, // 47: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest - 46, // 48: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest - 48, // 49: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest - 50, // 50: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest - 53, // 51: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest - 56, // 52: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest - 58, // 53: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest - 60, // 54: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest - 62, // 55: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest - 64, // 56: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest - 66, // 57: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest - 68, // 58: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest - 71, // 59: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest - 73, // 60: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest - 75, // 61: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest - 77, // 62: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest - 79, // 63: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest - 81, // 64: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest - 83, // 65: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest - 85, // 66: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest - 5, // 67: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest - 87, // 68: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest - 8, // 69: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 10, // 70: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 12, // 71: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 14, // 72: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 16, // 73: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 18, // 74: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 29, // 75: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse - 31, // 76: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse - 31, // 77: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse - 36, // 78: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse - 38, // 79: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 40, // 80: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 42, // 81: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 45, // 82: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse - 47, // 83: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse - 49, // 84: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse - 51, // 85: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse - 55, // 86: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse - 57, // 87: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent - 59, // 88: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse - 61, // 89: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse - 63, // 90: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse - 65, // 91: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse - 67, // 92: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse - 69, // 93: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse - 72, // 94: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse - 74, // 95: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse - 76, // 96: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse - 78, // 97: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse - 80, // 98: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse - 82, // 99: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse - 84, // 100: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse - 86, // 101: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse - 6, // 102: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse - 88, // 103: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse - 69, // [69:104] is the sub-list for method output_type - 34, // [34:69] is the sub-list for method input_type - 34, // [34:34] is the sub-list for extension type_name - 34, // [34:34] is the sub-list for extension extendee - 0, // [0:34] is the sub-list for field type_name + 44, // 23: daemon.ListStatesResponse.states:type_name -> daemon.State + 53, // 24: daemon.TracePacketRequest.tcp_flags:type_name -> daemon.TCPFlags + 55, // 25: daemon.TracePacketResponse.stages:type_name -> daemon.TraceStage + 3, // 26: daemon.SystemEvent.severity:type_name -> daemon.SystemEvent.Severity + 4, // 27: daemon.SystemEvent.category:type_name -> daemon.SystemEvent.Category + 97, // 28: daemon.SystemEvent.timestamp:type_name -> google.protobuf.Timestamp + 95, // 29: daemon.SystemEvent.metadata:type_name -> daemon.SystemEvent.MetadataEntry + 58, // 30: daemon.GetEventsResponse.events:type_name -> daemon.SystemEvent + 96, // 31: daemon.SetConfigRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 71, // 32: daemon.ListProfilesResponse.profiles:type_name -> daemon.Profile + 1, // 33: daemon.ExposeServiceRequest.protocol:type_name -> daemon.ExposeProtocol + 92, // 34: daemon.ExposeServiceEvent.ready:type_name -> daemon.ExposeServiceReady + 33, // 35: daemon.Network.ResolvedIPsEntry.value:type_name -> daemon.IPList + 8, // 36: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 10, // 37: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 12, // 38: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 14, // 39: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 16, // 40: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 18, // 41: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 29, // 42: daemon.DaemonService.ListNetworks:input_type -> daemon.ListNetworksRequest + 31, // 43: daemon.DaemonService.SelectNetworks:input_type -> daemon.SelectNetworksRequest + 31, // 44: daemon.DaemonService.DeselectNetworks:input_type -> daemon.SelectNetworksRequest + 5, // 45: daemon.DaemonService.ForwardingRules:input_type -> daemon.EmptyRequest + 38, // 46: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 40, // 47: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 42, // 48: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 45, // 49: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 47, // 50: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 49, // 51: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 51, // 52: daemon.DaemonService.SetSyncResponsePersistence:input_type -> daemon.SetSyncResponsePersistenceRequest + 54, // 53: daemon.DaemonService.TracePacket:input_type -> daemon.TracePacketRequest + 57, // 54: daemon.DaemonService.SubscribeEvents:input_type -> daemon.SubscribeRequest + 59, // 55: daemon.DaemonService.GetEvents:input_type -> daemon.GetEventsRequest + 61, // 56: daemon.DaemonService.SwitchProfile:input_type -> daemon.SwitchProfileRequest + 63, // 57: daemon.DaemonService.SetConfig:input_type -> daemon.SetConfigRequest + 65, // 58: daemon.DaemonService.AddProfile:input_type -> daemon.AddProfileRequest + 67, // 59: daemon.DaemonService.RemoveProfile:input_type -> daemon.RemoveProfileRequest + 69, // 60: daemon.DaemonService.ListProfiles:input_type -> daemon.ListProfilesRequest + 72, // 61: daemon.DaemonService.GetActiveProfile:input_type -> daemon.GetActiveProfileRequest + 74, // 62: daemon.DaemonService.Logout:input_type -> daemon.LogoutRequest + 76, // 63: daemon.DaemonService.GetFeatures:input_type -> daemon.GetFeaturesRequest + 78, // 64: daemon.DaemonService.GetPeerSSHHostKey:input_type -> daemon.GetPeerSSHHostKeyRequest + 80, // 65: daemon.DaemonService.RequestJWTAuth:input_type -> daemon.RequestJWTAuthRequest + 82, // 66: daemon.DaemonService.WaitJWTToken:input_type -> daemon.WaitJWTTokenRequest + 84, // 67: daemon.DaemonService.StartCPUProfile:input_type -> daemon.StartCPUProfileRequest + 86, // 68: daemon.DaemonService.StopCPUProfile:input_type -> daemon.StopCPUProfileRequest + 6, // 69: daemon.DaemonService.NotifyOSLifecycle:input_type -> daemon.OSLifecycleRequest + 88, // 70: daemon.DaemonService.GetInstallerResult:input_type -> daemon.InstallerResultRequest + 90, // 71: daemon.DaemonService.ExposeService:input_type -> daemon.ExposeServiceRequest + 9, // 72: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 11, // 73: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 13, // 74: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 15, // 75: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 17, // 76: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 19, // 77: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 30, // 78: daemon.DaemonService.ListNetworks:output_type -> daemon.ListNetworksResponse + 32, // 79: daemon.DaemonService.SelectNetworks:output_type -> daemon.SelectNetworksResponse + 32, // 80: daemon.DaemonService.DeselectNetworks:output_type -> daemon.SelectNetworksResponse + 37, // 81: daemon.DaemonService.ForwardingRules:output_type -> daemon.ForwardingRulesResponse + 39, // 82: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 41, // 83: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 43, // 84: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 46, // 85: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 48, // 86: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 50, // 87: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 52, // 88: daemon.DaemonService.SetSyncResponsePersistence:output_type -> daemon.SetSyncResponsePersistenceResponse + 56, // 89: daemon.DaemonService.TracePacket:output_type -> daemon.TracePacketResponse + 58, // 90: daemon.DaemonService.SubscribeEvents:output_type -> daemon.SystemEvent + 60, // 91: daemon.DaemonService.GetEvents:output_type -> daemon.GetEventsResponse + 62, // 92: daemon.DaemonService.SwitchProfile:output_type -> daemon.SwitchProfileResponse + 64, // 93: daemon.DaemonService.SetConfig:output_type -> daemon.SetConfigResponse + 66, // 94: daemon.DaemonService.AddProfile:output_type -> daemon.AddProfileResponse + 68, // 95: daemon.DaemonService.RemoveProfile:output_type -> daemon.RemoveProfileResponse + 70, // 96: daemon.DaemonService.ListProfiles:output_type -> daemon.ListProfilesResponse + 73, // 97: daemon.DaemonService.GetActiveProfile:output_type -> daemon.GetActiveProfileResponse + 75, // 98: daemon.DaemonService.Logout:output_type -> daemon.LogoutResponse + 77, // 99: daemon.DaemonService.GetFeatures:output_type -> daemon.GetFeaturesResponse + 79, // 100: daemon.DaemonService.GetPeerSSHHostKey:output_type -> daemon.GetPeerSSHHostKeyResponse + 81, // 101: daemon.DaemonService.RequestJWTAuth:output_type -> daemon.RequestJWTAuthResponse + 83, // 102: daemon.DaemonService.WaitJWTToken:output_type -> daemon.WaitJWTTokenResponse + 85, // 103: daemon.DaemonService.StartCPUProfile:output_type -> daemon.StartCPUProfileResponse + 87, // 104: daemon.DaemonService.StopCPUProfile:output_type -> daemon.StopCPUProfileResponse + 7, // 105: daemon.DaemonService.NotifyOSLifecycle:output_type -> daemon.OSLifecycleResponse + 89, // 106: daemon.DaemonService.GetInstallerResult:output_type -> daemon.InstallerResultResponse + 91, // 107: daemon.DaemonService.ExposeService:output_type -> daemon.ExposeServiceEvent + 72, // [72:108] is the sub-list for method output_type + 36, // [36:72] is the sub-list for method input_type + 36, // [36:36] is the sub-list for extension type_name + 36, // [36:36] is the sub-list for extension extendee + 0, // [0:36] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -6439,13 +6743,16 @@ func file_daemon_proto_init() { file_daemon_proto_msgTypes[58].OneofWrappers = []any{} file_daemon_proto_msgTypes[69].OneofWrappers = []any{} file_daemon_proto_msgTypes[75].OneofWrappers = []any{} + file_daemon_proto_msgTypes[86].OneofWrappers = []any{ + (*ExposeServiceEvent_Ready)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_proto_rawDesc), len(file_daemon_proto_rawDesc)), - NumEnums: 4, - NumMessages: 88, + NumEnums: 5, + NumMessages: 91, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 68b9a9348..4dc41d401 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -103,6 +103,9 @@ service DaemonService { rpc NotifyOSLifecycle(OSLifecycleRequest) returns(OSLifecycleResponse) {} rpc GetInstallerResult(InstallerResultRequest) returns (InstallerResultResponse) {} + + // ExposeService exposes a local port via the NetBird reverse proxy + rpc ExposeService(ExposeServiceRequest) returns (stream ExposeServiceEvent) {} } @@ -801,3 +804,32 @@ message InstallerResultResponse { bool success = 1; string errorMsg = 2; } + +enum ExposeProtocol { + EXPOSE_HTTP = 0; + EXPOSE_HTTPS = 1; + EXPOSE_TCP = 2; + EXPOSE_UDP = 3; +} + +message ExposeServiceRequest { + uint32 port = 1; + ExposeProtocol protocol = 2; + string pin = 3; + string password = 4; + repeated string user_groups = 5; + string domain = 6; + string name_prefix = 7; +} + +message ExposeServiceEvent { + oneof event { + ExposeServiceReady ready = 1; + } +} + +message ExposeServiceReady { + string service_name = 1; + string service_url = 2; + string domain = 3; +} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index ea9b4df05..4154dce59 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -76,6 +76,8 @@ type DaemonServiceClient interface { StopCPUProfile(ctx context.Context, in *StopCPUProfileRequest, opts ...grpc.CallOption) (*StopCPUProfileResponse, error) NotifyOSLifecycle(ctx context.Context, in *OSLifecycleRequest, opts ...grpc.CallOption) (*OSLifecycleResponse, error) GetInstallerResult(ctx context.Context, in *InstallerResultRequest, opts ...grpc.CallOption) (*InstallerResultResponse, error) + // ExposeService exposes a local port via the NetBird reverse proxy + ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error) } type daemonServiceClient struct { @@ -424,6 +426,38 @@ func (c *daemonServiceClient) GetInstallerResult(ctx context.Context, in *Instal return out, nil } +func (c *daemonServiceClient) ExposeService(ctx context.Context, in *ExposeServiceRequest, opts ...grpc.CallOption) (DaemonService_ExposeServiceClient, error) { + stream, err := c.cc.NewStream(ctx, &DaemonService_ServiceDesc.Streams[1], "/daemon.DaemonService/ExposeService", opts...) + if err != nil { + return nil, err + } + x := &daemonServiceExposeServiceClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type DaemonService_ExposeServiceClient interface { + Recv() (*ExposeServiceEvent, error) + grpc.ClientStream +} + +type daemonServiceExposeServiceClient struct { + grpc.ClientStream +} + +func (x *daemonServiceExposeServiceClient) Recv() (*ExposeServiceEvent, error) { + m := new(ExposeServiceEvent) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -486,6 +520,8 @@ type DaemonServiceServer interface { StopCPUProfile(context.Context, *StopCPUProfileRequest) (*StopCPUProfileResponse, error) NotifyOSLifecycle(context.Context, *OSLifecycleRequest) (*OSLifecycleResponse, error) GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) + // ExposeService exposes a local port via the NetBird reverse proxy + ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error mustEmbedUnimplementedDaemonServiceServer() } @@ -598,6 +634,9 @@ func (UnimplementedDaemonServiceServer) NotifyOSLifecycle(context.Context, *OSLi func (UnimplementedDaemonServiceServer) GetInstallerResult(context.Context, *InstallerResultRequest) (*InstallerResultResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetInstallerResult not implemented") } +func (UnimplementedDaemonServiceServer) ExposeService(*ExposeServiceRequest, DaemonService_ExposeServiceServer) error { + return status.Errorf(codes.Unimplemented, "method ExposeService not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -1244,6 +1283,27 @@ func _DaemonService_GetInstallerResult_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } +func _DaemonService_ExposeService_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ExposeServiceRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DaemonServiceServer).ExposeService(m, &daemonServiceExposeServiceServer{stream}) +} + +type DaemonService_ExposeServiceServer interface { + Send(*ExposeServiceEvent) error + grpc.ServerStream +} + +type daemonServiceExposeServiceServer struct { + grpc.ServerStream +} + +func (x *daemonServiceExposeServiceServer) Send(m *ExposeServiceEvent) error { + return x.ServerStream.SendMsg(m) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1394,6 +1454,11 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ Handler: _DaemonService_SubscribeEvents_Handler, ServerStreams: true, }, + { + StreamName: "ExposeService", + Handler: _DaemonService_ExposeService_Handler, + ServerStreams: true, + }, }, Metadata: "daemon.proto", } diff --git a/client/server/lifecycle.go b/client/server/lifecycle.go deleted file mode 100644 index 3722c027d..000000000 --- a/client/server/lifecycle.go +++ /dev/null @@ -1,77 +0,0 @@ -package server - -import ( - "context" - - log "github.com/sirupsen/logrus" - - "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/proto" -) - -// NotifyOSLifecycle handles operating system lifecycle events by executing appropriate logic based on the request type. -func (s *Server) NotifyOSLifecycle(callerCtx context.Context, req *proto.OSLifecycleRequest) (*proto.OSLifecycleResponse, error) { - switch req.GetType() { - case proto.OSLifecycleRequest_WAKEUP: - return s.handleWakeUp(callerCtx) - case proto.OSLifecycleRequest_SLEEP: - return s.handleSleep(callerCtx) - default: - log.Errorf("unknown OSLifecycleRequest type: %v", req.GetType()) - } - return &proto.OSLifecycleResponse{}, nil -} - -// handleWakeUp processes a wake-up event by triggering the Up command if the system was previously put to sleep. -// It resets the sleep state and logs the process. Returns a response or an error if the Up command fails. -func (s *Server) handleWakeUp(callerCtx context.Context) (*proto.OSLifecycleResponse, error) { - if !s.sleepTriggeredDown.Load() { - log.Info("skipping up because wasn't sleep down") - return &proto.OSLifecycleResponse{}, nil - } - - // avoid other wakeup runs if sleep didn't make the computer sleep - s.sleepTriggeredDown.Store(false) - - log.Info("running up after wake up") - _, err := s.Up(callerCtx, &proto.UpRequest{}) - if err != nil { - log.Errorf("running up failed: %v", err) - return &proto.OSLifecycleResponse{}, err - } - - log.Info("running up command executed successfully") - return &proto.OSLifecycleResponse{}, nil -} - -// handleSleep handles the sleep event by initiating a "down" sequence if the system is in a connected or connecting state. -func (s *Server) handleSleep(callerCtx context.Context) (*proto.OSLifecycleResponse, error) { - s.mutex.Lock() - - state := internal.CtxGetState(s.rootCtx) - status, err := state.Status() - if err != nil { - s.mutex.Unlock() - return &proto.OSLifecycleResponse{}, err - } - - if status != internal.StatusConnecting && status != internal.StatusConnected { - log.Infof("skipping setting the agent down because status is %s", status) - s.mutex.Unlock() - return &proto.OSLifecycleResponse{}, nil - } - s.mutex.Unlock() - - log.Info("running down after system started sleeping") - - _, err = s.Down(callerCtx, &proto.DownRequest{}) - if err != nil { - log.Errorf("running down failed: %v", err) - return &proto.OSLifecycleResponse{}, err - } - - s.sleepTriggeredDown.Store(true) - - log.Info("running down executed successfully") - return &proto.OSLifecycleResponse{}, nil -} diff --git a/client/server/lifecycle_test.go b/client/server/lifecycle_test.go deleted file mode 100644 index a604c60af..000000000 --- a/client/server/lifecycle_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package server - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/netbirdio/netbird/client/internal" - "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/proto" -) - -func newTestServer() *Server { - ctx := internal.CtxInitState(context.Background()) - return &Server{ - rootCtx: ctx, - statusRecorder: peer.NewRecorder(""), - } -} - -func TestNotifyOSLifecycle_WakeUp_SkipsWhenNotSleepTriggered(t *testing.T) { - s := newTestServer() - - // sleepTriggeredDown is false by default - assert.False(t, s.sleepTriggeredDown.Load()) - - resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_WAKEUP, - }) - - require.NoError(t, err) - require.NotNil(t, resp) - assert.False(t, s.sleepTriggeredDown.Load(), "flag should remain false") -} - -func TestNotifyOSLifecycle_Sleep_SkipsWhenStatusIdle(t *testing.T) { - s := newTestServer() - - state := internal.CtxGetState(s.rootCtx) - state.Set(internal.StatusIdle) - - resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_SLEEP, - }) - - require.NoError(t, err) - require.NotNil(t, resp) - assert.False(t, s.sleepTriggeredDown.Load(), "flag should remain false when status is Idle") -} - -func TestNotifyOSLifecycle_Sleep_SkipsWhenStatusNeedsLogin(t *testing.T) { - s := newTestServer() - - state := internal.CtxGetState(s.rootCtx) - state.Set(internal.StatusNeedsLogin) - - resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_SLEEP, - }) - - require.NoError(t, err) - require.NotNil(t, resp) - assert.False(t, s.sleepTriggeredDown.Load(), "flag should remain false when status is NeedsLogin") -} - -func TestNotifyOSLifecycle_Sleep_SetsFlag_WhenConnecting(t *testing.T) { - s := newTestServer() - - state := internal.CtxGetState(s.rootCtx) - state.Set(internal.StatusConnecting) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - s.actCancel = cancel - - resp, err := s.NotifyOSLifecycle(ctx, &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_SLEEP, - }) - - require.NoError(t, err) - assert.NotNil(t, resp, "handleSleep returns not nil response on success") - assert.True(t, s.sleepTriggeredDown.Load(), "flag should be set after sleep when connecting") -} - -func TestNotifyOSLifecycle_Sleep_SetsFlag_WhenConnected(t *testing.T) { - s := newTestServer() - - state := internal.CtxGetState(s.rootCtx) - state.Set(internal.StatusConnected) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - s.actCancel = cancel - - resp, err := s.NotifyOSLifecycle(ctx, &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_SLEEP, - }) - - require.NoError(t, err) - assert.NotNil(t, resp, "handleSleep returns not nil response on success") - assert.True(t, s.sleepTriggeredDown.Load(), "flag should be set after sleep when connected") -} - -func TestNotifyOSLifecycle_WakeUp_ResetsFlag(t *testing.T) { - s := newTestServer() - - // Manually set the flag to simulate prior sleep down - s.sleepTriggeredDown.Store(true) - - // WakeUp will try to call Up which fails without proper setup, but flag should reset first - _, _ = s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_WAKEUP, - }) - - assert.False(t, s.sleepTriggeredDown.Load(), "flag should be reset after WakeUp attempt") -} - -func TestNotifyOSLifecycle_MultipleWakeUpCalls(t *testing.T) { - s := newTestServer() - - // First wakeup without prior sleep - should be no-op - resp, err := s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_WAKEUP, - }) - require.NoError(t, err) - require.NotNil(t, resp) - assert.False(t, s.sleepTriggeredDown.Load()) - - // Simulate prior sleep - s.sleepTriggeredDown.Store(true) - - // First wakeup after sleep - should reset flag - _, _ = s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_WAKEUP, - }) - assert.False(t, s.sleepTriggeredDown.Load()) - - // Second wakeup - should be no-op - resp, err = s.NotifyOSLifecycle(context.Background(), &proto.OSLifecycleRequest{ - Type: proto.OSLifecycleRequest_WAKEUP, - }) - require.NoError(t, err) - require.NotNil(t, resp) - assert.False(t, s.sleepTriggeredDown.Load()) -} - -func TestHandleWakeUp_SkipsWhenFlagFalse(t *testing.T) { - s := newTestServer() - - resp, err := s.handleWakeUp(context.Background()) - - require.NoError(t, err) - require.NotNil(t, resp) -} - -func TestHandleWakeUp_ResetsFlagBeforeUp(t *testing.T) { - s := newTestServer() - s.sleepTriggeredDown.Store(true) - - // Even if Up fails, flag should be reset - _, _ = s.handleWakeUp(context.Background()) - - assert.False(t, s.sleepTriggeredDown.Load(), "flag must be reset before calling Up") -} - -func TestHandleSleep_SkipsForNonActiveStates(t *testing.T) { - tests := []struct { - name string - status internal.StatusType - }{ - {"Idle", internal.StatusIdle}, - {"NeedsLogin", internal.StatusNeedsLogin}, - {"LoginFailed", internal.StatusLoginFailed}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := newTestServer() - state := internal.CtxGetState(s.rootCtx) - state.Set(tt.status) - - resp, err := s.handleSleep(context.Background()) - - require.NoError(t, err) - require.NotNil(t, resp) - assert.False(t, s.sleepTriggeredDown.Load()) - }) - } -} - -func TestHandleSleep_ProceedsForActiveStates(t *testing.T) { - tests := []struct { - name string - status internal.StatusType - }{ - {"Connecting", internal.StatusConnecting}, - {"Connected", internal.StatusConnected}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := newTestServer() - state := internal.CtxGetState(s.rootCtx) - state.Set(tt.status) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - s.actCancel = cancel - - resp, err := s.handleSleep(ctx) - - require.NoError(t, err) - assert.NotNil(t, resp) - assert.True(t, s.sleepTriggeredDown.Load()) - }) - } -} diff --git a/client/server/server.go b/client/server/server.go index 108eab9fe..0466630c5 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -21,7 +21,9 @@ import ( gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/internal/auth" + "github.com/netbirdio/netbird/client/internal/expose" "github.com/netbirdio/netbird/client/internal/profilemanager" + sleephandler "github.com/netbirdio/netbird/client/internal/sleep/handler" "github.com/netbirdio/netbird/client/system" mgm "github.com/netbirdio/netbird/shared/management/client" "github.com/netbirdio/netbird/shared/management/domain" @@ -85,8 +87,7 @@ type Server struct { profilesDisabled bool updateSettingsDisabled bool - // sleepTriggeredDown holds a state indicated if the sleep handler triggered the last client down - sleepTriggeredDown atomic.Bool + sleepHandler *sleephandler.SleepHandler jwtCache *jwtCache } @@ -100,7 +101,7 @@ type oauthAuthFlow struct { // New server instance constructor. func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool) *Server { - return &Server{ + s := &Server{ rootCtx: ctx, logFile: logFile, persistSyncResponse: true, @@ -110,6 +111,10 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable updateSettingsDisabled: updateSettingsDisabled, jwtCache: newJWTCache(), } + agent := &serverAgent{s} + s.sleepHandler = sleephandler.New(agent) + + return s } func (s *Server) Start() error { @@ -1312,6 +1317,60 @@ func (s *Server) WaitJWTToken( }, nil } +// ExposeService exposes a local port via the NetBird reverse proxy. +func (s *Server) ExposeService(req *proto.ExposeServiceRequest, srv proto.DaemonService_ExposeServiceServer) error { + s.mutex.Lock() + if !s.clientRunning { + s.mutex.Unlock() + return gstatus.Errorf(codes.FailedPrecondition, "client is not running, run 'netbird up' first") + } + connectClient := s.connectClient + s.mutex.Unlock() + + if connectClient == nil { + return gstatus.Errorf(codes.FailedPrecondition, "client not initialized") + } + + engine := connectClient.Engine() + if engine == nil { + return gstatus.Errorf(codes.FailedPrecondition, "engine not initialized") + } + + mgr := engine.GetExposeManager() + if mgr == nil { + return gstatus.Errorf(codes.Internal, "expose manager not available") + } + + ctx := srv.Context() + + exposeCtx, exposeCancel := context.WithTimeout(ctx, 30*time.Second) + defer exposeCancel() + + mgmReq := expose.NewRequest(req) + result, err := mgr.Expose(exposeCtx, *mgmReq) + if err != nil { + return err + } + + if err := srv.Send(&proto.ExposeServiceEvent{ + Event: &proto.ExposeServiceEvent_Ready{ + Ready: &proto.ExposeServiceReady{ + ServiceName: result.ServiceName, + ServiceUrl: result.ServiceURL, + Domain: result.Domain, + }, + }, + }); err != nil { + return err + } + + err = mgr.KeepAlive(ctx, result.Domain) + if err != nil { + return err + } + return nil +} + func isUnixRunningDesktop() bool { if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" { return false diff --git a/client/server/sleep.go b/client/server/sleep.go new file mode 100644 index 000000000..7a83c75a6 --- /dev/null +++ b/client/server/sleep.go @@ -0,0 +1,46 @@ +package server + +import ( + "context" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/proto" +) + +// serverAgent adapts Server to the handler.Agent and handler.StatusChecker interfaces +type serverAgent struct { + s *Server +} + +func (a *serverAgent) Up(ctx context.Context) error { + _, err := a.s.Up(ctx, &proto.UpRequest{}) + return err +} + +func (a *serverAgent) Down(ctx context.Context) error { + _, err := a.s.Down(ctx, &proto.DownRequest{}) + return err +} + +func (a *serverAgent) Status() (internal.StatusType, error) { + return internal.CtxGetState(a.s.rootCtx).Status() +} + +// NotifyOSLifecycle handles operating system lifecycle events by executing appropriate logic based on the request type. +func (s *Server) NotifyOSLifecycle(callerCtx context.Context, req *proto.OSLifecycleRequest) (*proto.OSLifecycleResponse, error) { + switch req.GetType() { + case proto.OSLifecycleRequest_WAKEUP: + if err := s.sleepHandler.HandleWakeUp(callerCtx); err != nil { + return &proto.OSLifecycleResponse{}, err + } + case proto.OSLifecycleRequest_SLEEP: + if err := s.sleepHandler.HandleSleep(callerCtx); err != nil { + return &proto.OSLifecycleResponse{}, err + } + default: + log.Errorf("unknown OSLifecycleRequest type: %v", req.GetType()) + } + return &proto.OSLifecycleResponse{}, nil +} diff --git a/management/internals/modules/reverseproxy/domain/interface.go b/management/internals/modules/reverseproxy/domain/interface.go index d40e9b637..a4bba5841 100644 --- a/management/internals/modules/reverseproxy/domain/interface.go +++ b/management/internals/modules/reverseproxy/domain/interface.go @@ -9,4 +9,5 @@ type Manager interface { CreateDomain(ctx context.Context, accountID, userID, domainName, targetCluster string) (*Domain, error) DeleteDomain(ctx context.Context, accountID, userID, domainID string) error ValidateDomain(ctx context.Context, accountID, userID, domainID string) + GetClusterDomains() []string } diff --git a/management/internals/modules/reverseproxy/domain/manager/manager.go b/management/internals/modules/reverseproxy/domain/manager/manager.go index c1e87a70f..12dd051fd 100644 --- a/management/internals/modules/reverseproxy/domain/manager/manager.go +++ b/management/internals/modules/reverseproxy/domain/manager/manager.go @@ -228,6 +228,18 @@ func (m Manager) ValidateDomain(ctx context.Context, accountID, userID, domainID } } +// GetClusterDomains returns a list of proxy cluster domains. +func (m Manager) GetClusterDomains() []string { + if m.proxyManager == nil { + return nil + } + addresses, err := m.proxyManager.GetActiveClusterAddresses(context.Background()) + if err != nil { + return nil + } + return addresses +} + // DeriveClusterFromDomain determines the proxy cluster for a given domain. // For free domains (those ending with a known cluster suffix), the cluster is extracted from the domain. // For custom domains, the cluster is determined by checking the registered custom domain's target cluster. diff --git a/management/internals/modules/reverseproxy/service/interface.go b/management/internals/modules/reverseproxy/service/interface.go index a20f3049b..52095db6b 100644 --- a/management/internals/modules/reverseproxy/service/interface.go +++ b/management/internals/modules/reverseproxy/service/interface.go @@ -23,6 +23,10 @@ type Manager interface { GetServiceByID(ctx context.Context, accountID, serviceID string) (*Service, error) GetAccountServices(ctx context.Context, accountID string) ([]*Service, error) GetServiceIDByTargetID(ctx context.Context, accountID string, resourceID string) (string, error) + CreateServiceFromPeer(ctx context.Context, accountID, peerID string, req *ExposeServiceRequest) (*ExposeServiceResponse, error) + RenewServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error + StopServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error + StartExposeReaper(ctx context.Context) } // ProxyController is responsible for managing proxy clusters and routing service updates. diff --git a/management/internals/modules/reverseproxy/service/interface_mock.go b/management/internals/modules/reverseproxy/service/interface_mock.go index e8aa9eb34..41acb77de 100644 --- a/management/internals/modules/reverseproxy/service/interface_mock.go +++ b/management/internals/modules/reverseproxy/service/interface_mock.go @@ -50,6 +50,21 @@ func (mr *MockManagerMockRecorder) CreateService(ctx, accountID, userID, service return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateService", reflect.TypeOf((*MockManager)(nil).CreateService), ctx, accountID, userID, service) } +// CreateServiceFromPeer mocks base method. +func (m *MockManager) CreateServiceFromPeer(ctx context.Context, accountID, peerID string, req *ExposeServiceRequest) (*ExposeServiceResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateServiceFromPeer", ctx, accountID, peerID, req) + ret0, _ := ret[0].(*ExposeServiceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateServiceFromPeer indicates an expected call of CreateServiceFromPeer. +func (mr *MockManagerMockRecorder) CreateServiceFromPeer(ctx, accountID, peerID, req interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServiceFromPeer", reflect.TypeOf((*MockManager)(nil).CreateServiceFromPeer), ctx, accountID, peerID, req) +} + // DeleteAllServices mocks base method. func (m *MockManager) DeleteAllServices(ctx context.Context, accountID, userID string) error { m.ctrl.T.Helper() @@ -196,6 +211,20 @@ func (mr *MockManagerMockRecorder) ReloadService(ctx, accountID, serviceID inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadService", reflect.TypeOf((*MockManager)(nil).ReloadService), ctx, accountID, serviceID) } +// RenewServiceFromPeer mocks base method. +func (m *MockManager) RenewServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RenewServiceFromPeer", ctx, accountID, peerID, domain) + ret0, _ := ret[0].(error) + return ret0 +} + +// RenewServiceFromPeer indicates an expected call of RenewServiceFromPeer. +func (mr *MockManagerMockRecorder) RenewServiceFromPeer(ctx, accountID, peerID, domain interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewServiceFromPeer", reflect.TypeOf((*MockManager)(nil).RenewServiceFromPeer), ctx, accountID, peerID, domain) +} + // SetCertificateIssuedAt mocks base method. func (m *MockManager) SetCertificateIssuedAt(ctx context.Context, accountID, serviceID string) error { m.ctrl.T.Helper() @@ -224,6 +253,32 @@ func (mr *MockManagerMockRecorder) SetStatus(ctx, accountID, serviceID, status i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatus", reflect.TypeOf((*MockManager)(nil).SetStatus), ctx, accountID, serviceID, status) } +// StartExposeReaper mocks base method. +func (m *MockManager) StartExposeReaper(ctx context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StartExposeReaper", ctx) +} + +// StartExposeReaper indicates an expected call of StartExposeReaper. +func (mr *MockManagerMockRecorder) StartExposeReaper(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartExposeReaper", reflect.TypeOf((*MockManager)(nil).StartExposeReaper), ctx) +} + +// StopServiceFromPeer mocks base method. +func (m *MockManager) StopServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StopServiceFromPeer", ctx, accountID, peerID, domain) + ret0, _ := ret[0].(error) + return ret0 +} + +// StopServiceFromPeer indicates an expected call of StopServiceFromPeer. +func (mr *MockManagerMockRecorder) StopServiceFromPeer(ctx, accountID, peerID, domain interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopServiceFromPeer", reflect.TypeOf((*MockManager)(nil).StopServiceFromPeer), ctx, accountID, peerID, domain) +} + // UpdateService mocks base method. func (m *MockManager) UpdateService(ctx context.Context, accountID, userID string, service *Service) (*Service, error) { m.ctrl.T.Helper() diff --git a/management/internals/modules/reverseproxy/service/manager/expose_tracker.go b/management/internals/modules/reverseproxy/service/manager/expose_tracker.go new file mode 100644 index 000000000..11e1f0110 --- /dev/null +++ b/management/internals/modules/reverseproxy/service/manager/expose_tracker.go @@ -0,0 +1,163 @@ +package manager + +import ( + "context" + "sync" + "time" + + "github.com/netbirdio/netbird/shared/management/status" + log "github.com/sirupsen/logrus" +) + +const ( + exposeTTL = 90 * time.Second + exposeReapInterval = 30 * time.Second + maxExposesPerPeer = 10 +) + +type trackedExpose struct { + mu sync.Mutex + domain string + accountID string + peerID string + lastRenewed time.Time + expiring bool +} + +type exposeTracker struct { + activeExposes sync.Map + exposeCreateMu sync.Mutex + manager *Manager +} + +func exposeKey(peerID, domain string) string { + return peerID + ":" + domain +} + +// TrackExposeIfAllowed atomically checks the per-peer limit and registers a new +// active expose session under the same lock. Returns (true, false) if the expose +// was already tracked (duplicate), (false, true) if tracking succeeded, and +// (false, false) if the peer has reached the limit. +func (t *exposeTracker) TrackExposeIfAllowed(peerID, domain, accountID string) (alreadyTracked, ok bool) { + t.exposeCreateMu.Lock() + defer t.exposeCreateMu.Unlock() + + key := exposeKey(peerID, domain) + _, loaded := t.activeExposes.LoadOrStore(key, &trackedExpose{ + domain: domain, + accountID: accountID, + peerID: peerID, + lastRenewed: time.Now(), + }) + if loaded { + return true, false + } + + if t.CountPeerExposes(peerID) > maxExposesPerPeer { + t.activeExposes.Delete(key) + return false, false + } + + return false, true +} + +// UntrackExpose removes an active expose session from tracking. +func (t *exposeTracker) UntrackExpose(peerID, domain string) { + t.activeExposes.Delete(exposeKey(peerID, domain)) +} + +// CountPeerExposes returns the number of active expose sessions for a peer. +func (t *exposeTracker) CountPeerExposes(peerID string) int { + count := 0 + t.activeExposes.Range(func(_, val any) bool { + if expose := val.(*trackedExpose); expose.peerID == peerID { + count++ + } + return true + }) + return count +} + +// MaxExposesPerPeer returns the maximum number of concurrent exposes allowed per peer. +func (t *exposeTracker) MaxExposesPerPeer() int { + return maxExposesPerPeer +} + +// RenewTrackedExpose updates the in-memory lastRenewed timestamp for a tracked expose. +// Returns false if the expose is not tracked or is being reaped. +func (t *exposeTracker) RenewTrackedExpose(peerID, domain string) bool { + key := exposeKey(peerID, domain) + val, ok := t.activeExposes.Load(key) + if !ok { + return false + } + + expose := val.(*trackedExpose) + expose.mu.Lock() + if expose.expiring { + expose.mu.Unlock() + return false + } + expose.lastRenewed = time.Now() + expose.mu.Unlock() + + return true +} + +// StopTrackedExpose removes an active expose session from tracking. +// Returns false if the expose was not tracked. +func (t *exposeTracker) StopTrackedExpose(peerID, domain string) bool { + key := exposeKey(peerID, domain) + _, ok := t.activeExposes.LoadAndDelete(key) + return ok +} + +// StartExposeReaper starts a background goroutine that reaps expired expose sessions. +func (t *exposeTracker) StartExposeReaper(ctx context.Context) { + go func() { + ticker := time.NewTicker(exposeReapInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + t.reapExpiredExposes() + } + } + }() +} + +func (t *exposeTracker) reapExpiredExposes() { + t.activeExposes.Range(func(key, val any) bool { + expose := val.(*trackedExpose) + expose.mu.Lock() + expired := time.Since(expose.lastRenewed) > exposeTTL + if expired { + expose.expiring = true + } + expose.mu.Unlock() + + if !expired { + return true + } + + log.Infof("reaping expired expose session for peer %s, domain %s", expose.peerID, expose.domain) + + err := t.manager.deleteServiceFromPeer(context.Background(), expose.accountID, expose.peerID, expose.domain, true) + + s, _ := status.FromError(err) + + switch { + case err == nil: + t.activeExposes.Delete(key) + case s.ErrorType == status.NotFound: + log.Debugf("service %s was already deleted", expose.domain) + default: + log.Errorf("failed to delete expired peer-exposed service for domain %s: %v", expose.domain, err) + } + + return true + }) +} diff --git a/management/internals/modules/reverseproxy/service/manager/expose_tracker_test.go b/management/internals/modules/reverseproxy/service/manager/expose_tracker_test.go new file mode 100644 index 000000000..154239fb1 --- /dev/null +++ b/management/internals/modules/reverseproxy/service/manager/expose_tracker_test.go @@ -0,0 +1,256 @@ +package manager + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" +) + +func TestExposeKey(t *testing.T) { + assert.Equal(t, "peer1:example.com", exposeKey("peer1", "example.com")) + assert.Equal(t, "peer2:other.com", exposeKey("peer2", "other.com")) + assert.NotEqual(t, exposeKey("peer1", "a.com"), exposeKey("peer1", "b.com")) +} + +func TestTrackExposeIfAllowed(t *testing.T) { + t.Run("first track succeeds", func(t *testing.T) { + tracker := &exposeTracker{} + alreadyTracked, ok := tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + assert.False(t, alreadyTracked, "first track should not be duplicate") + assert.True(t, ok, "first track should be allowed") + }) + + t.Run("duplicate track detected", func(t *testing.T) { + tracker := &exposeTracker{} + tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + + alreadyTracked, ok := tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + assert.True(t, alreadyTracked, "second track should be duplicate") + assert.False(t, ok) + }) + + t.Run("rejects when at limit", func(t *testing.T) { + tracker := &exposeTracker{} + for i := range maxExposesPerPeer { + _, ok := tracker.TrackExposeIfAllowed("peer1", "domain-"+string(rune('a'+i))+".com", "acct1") + assert.True(t, ok, "track %d should be allowed", i) + } + + alreadyTracked, ok := tracker.TrackExposeIfAllowed("peer1", "over-limit.com", "acct1") + assert.False(t, alreadyTracked) + assert.False(t, ok, "should reject when at limit") + }) + + t.Run("other peer unaffected by limit", func(t *testing.T) { + tracker := &exposeTracker{} + for i := range maxExposesPerPeer { + tracker.TrackExposeIfAllowed("peer1", "domain-"+string(rune('a'+i))+".com", "acct1") + } + + _, ok := tracker.TrackExposeIfAllowed("peer2", "a.com", "acct1") + assert.True(t, ok, "other peer should still be within limit") + }) +} + +func TestUntrackExpose(t *testing.T) { + tracker := &exposeTracker{} + + tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + assert.Equal(t, 1, tracker.CountPeerExposes("peer1")) + + tracker.UntrackExpose("peer1", "a.com") + assert.Equal(t, 0, tracker.CountPeerExposes("peer1")) +} + +func TestCountPeerExposes(t *testing.T) { + tracker := &exposeTracker{} + + assert.Equal(t, 0, tracker.CountPeerExposes("peer1")) + + tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + tracker.TrackExposeIfAllowed("peer1", "b.com", "acct1") + tracker.TrackExposeIfAllowed("peer2", "a.com", "acct1") + + assert.Equal(t, 2, tracker.CountPeerExposes("peer1"), "peer1 should have 2 exposes") + assert.Equal(t, 1, tracker.CountPeerExposes("peer2"), "peer2 should have 1 expose") + assert.Equal(t, 0, tracker.CountPeerExposes("peer3"), "peer3 should have 0 exposes") +} + +func TestMaxExposesPerPeer(t *testing.T) { + tracker := &exposeTracker{} + assert.Equal(t, maxExposesPerPeer, tracker.MaxExposesPerPeer()) +} + +func TestRenewTrackedExpose(t *testing.T) { + tracker := &exposeTracker{} + + found := tracker.RenewTrackedExpose("peer1", "a.com") + assert.False(t, found, "should not find untracked expose") + + tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + + found = tracker.RenewTrackedExpose("peer1", "a.com") + assert.True(t, found, "should find tracked expose") +} + +func TestRenewTrackedExpose_RejectsExpiring(t *testing.T) { + tracker := &exposeTracker{} + tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1") + + // Simulate reaper marking the expose as expiring + key := exposeKey("peer1", "a.com") + val, _ := tracker.activeExposes.Load(key) + expose := val.(*trackedExpose) + expose.mu.Lock() + expose.expiring = true + expose.mu.Unlock() + + found := tracker.RenewTrackedExpose("peer1", "a.com") + assert.False(t, found, "should reject renewal when expiring") +} + +func TestReapExpiredExposes(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + tracker := mgr.exposeTracker + + ctx := context.Background() + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + }) + require.NoError(t, err) + + // Manually expire the tracked entry + key := exposeKey(testPeerID, resp.Domain) + val, _ := tracker.activeExposes.Load(key) + expose := val.(*trackedExpose) + expose.mu.Lock() + expose.lastRenewed = time.Now().Add(-2 * exposeTTL) + expose.mu.Unlock() + + // Add an active (non-expired) tracking entry + tracker.activeExposes.Store(exposeKey("peer1", "active.com"), &trackedExpose{ + domain: "active.com", + accountID: testAccountID, + peerID: "peer1", + lastRenewed: time.Now(), + }) + + tracker.reapExpiredExposes() + + _, exists := tracker.activeExposes.Load(key) + assert.False(t, exists, "expired expose should be removed") + + _, exists = tracker.activeExposes.Load(exposeKey("peer1", "active.com")) + assert.True(t, exists, "active expose should remain") +} + +func TestReapExpiredExposes_SetsExpiringFlag(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + tracker := mgr.exposeTracker + + ctx := context.Background() + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + }) + require.NoError(t, err) + + key := exposeKey(testPeerID, resp.Domain) + val, _ := tracker.activeExposes.Load(key) + expose := val.(*trackedExpose) + + // Expire it + expose.mu.Lock() + expose.lastRenewed = time.Now().Add(-2 * exposeTTL) + expose.mu.Unlock() + + // Renew should succeed before reaping + assert.True(t, tracker.RenewTrackedExpose(testPeerID, resp.Domain), "renew should succeed before reaper runs") + + // Re-expire and reap + expose.mu.Lock() + expose.lastRenewed = time.Now().Add(-2 * exposeTTL) + expose.mu.Unlock() + + tracker.reapExpiredExposes() + + // Entry is deleted, renew returns false + assert.False(t, tracker.RenewTrackedExpose(testPeerID, resp.Domain), "renew should fail after reap") +} + +func TestConcurrentTrackAndCount(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + tracker := mgr.exposeTracker + ctx := context.Background() + + for i := range 5 { + _, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080 + i, + Protocol: "http", + }) + require.NoError(t, err) + } + + // Manually expire all tracked entries + tracker.activeExposes.Range(func(_, val any) bool { + expose := val.(*trackedExpose) + expose.mu.Lock() + expose.lastRenewed = time.Now().Add(-2 * exposeTTL) + expose.mu.Unlock() + return true + }) + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + tracker.reapExpiredExposes() + }() + go func() { + defer wg.Done() + tracker.CountPeerExposes(testPeerID) + }() + wg.Wait() + + assert.Equal(t, 0, tracker.CountPeerExposes(testPeerID), "all expired exposes should be reaped") +} + +func TestTrackedExposeMutexProtectsLastRenewed(t *testing.T) { + expose := &trackedExpose{ + lastRenewed: time.Now().Add(-1 * time.Hour), + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + for range 100 { + expose.mu.Lock() + expose.lastRenewed = time.Now() + expose.mu.Unlock() + } + }() + + go func() { + defer wg.Done() + for range 100 { + expose.mu.Lock() + _ = time.Since(expose.lastRenewed) + expose.mu.Unlock() + } + }() + + wg.Wait() + + expose.mu.Lock() + require.False(t, expose.lastRenewed.IsZero(), "lastRenewed should not be zero after concurrent access") + expose.mu.Unlock() +} diff --git a/management/internals/modules/reverseproxy/service/manager/manager.go b/management/internals/modules/reverseproxy/service/manager/manager.go index 547922982..9adb1766e 100644 --- a/management/internals/modules/reverseproxy/service/manager/manager.go +++ b/management/internals/modules/reverseproxy/service/manager/manager.go @@ -3,10 +3,14 @@ package manager import ( "context" "fmt" + "math/rand/v2" + "slices" "time" log "github.com/sirupsen/logrus" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey" "github.com/netbirdio/netbird/management/server/account" @@ -23,6 +27,7 @@ const unknownHostPlaceholder = "unknown" // ClusterDeriver derives the proxy cluster from a domain. type ClusterDeriver interface { DeriveClusterFromDomain(ctx context.Context, accountID, domain string) (string, error) + GetClusterDomains() []string } type Manager struct { @@ -31,17 +36,25 @@ type Manager struct { permissionsManager permissions.Manager proxyController service.ProxyController clusterDeriver ClusterDeriver + exposeTracker *exposeTracker } // NewManager creates a new service manager. func NewManager(store store.Store, accountManager account.Manager, permissionsManager permissions.Manager, proxyController service.ProxyController, clusterDeriver ClusterDeriver) *Manager { - return &Manager{ + mgr := &Manager{ store: store, accountManager: accountManager, permissionsManager: permissionsManager, proxyController: proxyController, clusterDeriver: clusterDeriver, } + mgr.exposeTracker = &exposeTracker{manager: mgr} + return mgr +} + +// StartExposeReaper delegates to the expose tracker. +func (m *Manager) StartExposeReaper(ctx context.Context) { + m.exposeTracker.StartExposeReaper(ctx) } func (m *Manager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) { @@ -394,6 +407,10 @@ func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceI return err } + if s.Source == service.SourceEphemeral { + m.exposeTracker.UntrackExpose(s.SourcePeer, s.Domain) + } + m.accountManager.StoreEvent(ctx, userID, serviceID, accountID, activity.ServiceDeleted, s.EventMeta()) m.proxyController.SendServiceUpdateToCluster(ctx, accountID, s.ToProtoMapping(service.Delete, "", m.proxyController.GetOIDCValidationConfig()), s.ProxyCluster) @@ -435,6 +452,9 @@ func (m *Manager) DeleteAllServices(ctx context.Context, accountID, userID strin oidcCfg := m.proxyController.GetOIDCValidationConfig() for _, svc := range services { + if svc.Source == service.SourceEphemeral { + m.exposeTracker.UntrackExpose(svc.SourcePeer, svc.Domain) + } m.accountManager.StoreEvent(ctx, userID, svc.ID, accountID, activity.ServiceDeleted, svc.EventMeta()) m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Delete, "", oidcCfg), svc.ProxyCluster) } @@ -453,7 +473,8 @@ func (m *Manager) SetCertificateIssuedAt(ctx context.Context, accountID, service return fmt.Errorf("failed to get service: %w", err) } - service.Meta.CertificateIssuedAt = time.Now() + now := time.Now() + service.Meta.CertificateIssuedAt = &now if err = transaction.UpdateService(ctx, service); err != nil { return fmt.Errorf("failed to update service certificate timestamp: %w", err) @@ -577,3 +598,258 @@ func (m *Manager) GetServiceIDByTargetID(ctx context.Context, accountID string, return target.ServiceID, nil } + +// validateExposePermission checks whether the peer is allowed to use the expose feature. +// It verifies the account has peer expose enabled and that the peer belongs to an allowed group. +func (m *Manager) validateExposePermission(ctx context.Context, accountID, peerID string) error { + settings, err := m.store.GetAccountSettings(ctx, store.LockingStrengthNone, accountID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get account settings: %v", err) + return status.Errorf(status.Internal, "get account settings: %v", err) + } + + if !settings.PeerExposeEnabled { + return status.Errorf(status.PermissionDenied, "peer expose is not enabled for this account") + } + + if len(settings.PeerExposeGroups) == 0 { + return status.Errorf(status.PermissionDenied, "no group is set for peer expose") + } + + peerGroupIDs, err := m.store.GetPeerGroupIDs(ctx, store.LockingStrengthNone, accountID, peerID) + if err != nil { + log.WithContext(ctx).Errorf("failed to get peer group IDs: %v", err) + return status.Errorf(status.Internal, "get peer groups: %v", err) + } + + for _, pg := range peerGroupIDs { + if slices.Contains(settings.PeerExposeGroups, pg) { + return nil + } + } + + return status.Errorf(status.PermissionDenied, "peer is not in an allowed expose group") +} + +// CreateServiceFromPeer creates a service initiated by a peer expose request. +// It validates the request, checks expose permissions, enforces the per-peer limit, +// creates the service, and tracks it for TTL-based reaping. +func (m *Manager) CreateServiceFromPeer(ctx context.Context, accountID, peerID string, req *service.ExposeServiceRequest) (*service.ExposeServiceResponse, error) { + if err := req.Validate(); err != nil { + return nil, status.Errorf(status.InvalidArgument, "validate expose request: %v", err) + } + + if err := m.validateExposePermission(ctx, accountID, peerID); err != nil { + return nil, err + } + + serviceName, err := service.GenerateExposeName(req.NamePrefix) + if err != nil { + return nil, status.Errorf(status.InvalidArgument, "generate service name: %v", err) + } + + svc := req.ToService(accountID, peerID, serviceName) + svc.Source = service.SourceEphemeral + + if svc.Domain == "" { + domain, err := m.buildRandomDomain(svc.Name) + if err != nil { + return nil, fmt.Errorf("build random domain for service %s: %w", svc.Name, err) + } + svc.Domain = domain + } + + if svc.Auth.BearerAuth != nil && svc.Auth.BearerAuth.Enabled { + groupIDs, err := m.getGroupIDsFromNames(ctx, accountID, svc.Auth.BearerAuth.DistributionGroups) + if err != nil { + return nil, fmt.Errorf("get group ids for service %s: %w", svc.Name, err) + } + svc.Auth.BearerAuth.DistributionGroups = groupIDs + } + + if err := m.initializeServiceForCreate(ctx, accountID, svc); err != nil { + return nil, err + } + + peer, err := m.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID) + if err != nil { + return nil, err + } + + now := time.Now() + svc.Meta.LastRenewedAt = &now + svc.SourcePeer = peerID + + if err := m.persistNewService(ctx, accountID, svc); err != nil { + return nil, err + } + + alreadyTracked, allowed := m.exposeTracker.TrackExposeIfAllowed(peerID, svc.Domain, accountID) + if alreadyTracked { + if err := m.deleteServiceFromPeer(ctx, accountID, peerID, svc.Domain, false); err != nil { + log.WithContext(ctx).Debugf("failed to delete duplicate expose service for domain %s: %v", svc.Domain, err) + } + return nil, status.Errorf(status.AlreadyExists, "peer already has an active expose session for this domain") + } + if !allowed { + if err := m.deleteServiceFromPeer(ctx, accountID, peerID, svc.Domain, false); err != nil { + log.WithContext(ctx).Debugf("failed to delete service after limit exceeded for domain %s: %v", svc.Domain, err) + } + return nil, status.Errorf(status.PreconditionFailed, "peer has reached the maximum number of active expose sessions (%d)", maxExposesPerPeer) + } + + meta := addPeerInfoToEventMeta(svc.EventMeta(), peer) + m.accountManager.StoreEvent(ctx, peerID, svc.ID, accountID, activity.PeerServiceExposed, meta) + + if err := m.replaceHostByLookup(ctx, accountID, svc); err != nil { + return nil, fmt.Errorf("replace host by lookup for service %s: %w", svc.ID, err) + } + + m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Create, "", m.proxyController.GetOIDCValidationConfig()), svc.ProxyCluster) + m.accountManager.UpdateAccountPeers(ctx, accountID) + + return &service.ExposeServiceResponse{ + ServiceName: svc.Name, + ServiceURL: "https://" + svc.Domain, + Domain: svc.Domain, + }, nil +} + +func (m *Manager) getGroupIDsFromNames(ctx context.Context, accountID string, groupNames []string) ([]string, error) { + if len(groupNames) == 0 { + return []string{}, fmt.Errorf("no group names provided") + } + groupIDs := make([]string, 0, len(groupNames)) + for _, groupName := range groupNames { + g, err := m.accountManager.GetGroupByName(ctx, groupName, accountID) + if err != nil { + return nil, fmt.Errorf("failed to get group by name %s: %w", groupName, err) + } + groupIDs = append(groupIDs, g.ID) + } + return groupIDs, nil +} + +func (m *Manager) buildRandomDomain(name string) (string, error) { + if m.clusterDeriver == nil { + return "", fmt.Errorf("unable to get random domain") + } + clusterDomains := m.clusterDeriver.GetClusterDomains() + if len(clusterDomains) == 0 { + return "", fmt.Errorf("no cluster domains found for service %s", name) + } + index := rand.IntN(len(clusterDomains)) + domain := name + "." + clusterDomains[index] + return domain, nil +} + +// RenewServiceFromPeer renews the in-memory TTL tracker for the peer's expose session. +// Returns an error if the expose is not actively tracked. +func (m *Manager) RenewServiceFromPeer(_ context.Context, _, peerID, domain string) error { + if !m.exposeTracker.RenewTrackedExpose(peerID, domain) { + return status.Errorf(status.NotFound, "no active expose session for domain %s", domain) + } + return nil +} + +// StopServiceFromPeer stops a peer's active expose session by untracking and deleting the service. +func (m *Manager) StopServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error { + if err := m.deleteServiceFromPeer(ctx, accountID, peerID, domain, false); err != nil { + log.WithContext(ctx).Errorf("failed to delete peer-exposed service for domain %s: %v", domain, err) + return err + } + + if !m.exposeTracker.StopTrackedExpose(peerID, domain) { + log.WithContext(ctx).Warnf("expose tracker entry for domain %s already removed; service was deleted", domain) + } + + return nil +} + +// deleteServiceFromPeer deletes a peer-initiated service identified by domain. +// When expired is true, the activity is recorded as PeerServiceExposeExpired instead of PeerServiceUnexposed. +func (m *Manager) deleteServiceFromPeer(ctx context.Context, accountID, peerID, domain string, expired bool) error { + svc, err := m.lookupPeerService(ctx, accountID, peerID, domain) + if err != nil { + return err + } + + activityCode := activity.PeerServiceUnexposed + if expired { + activityCode = activity.PeerServiceExposeExpired + } + return m.deletePeerService(ctx, accountID, peerID, svc.ID, activityCode) +} + +// lookupPeerService finds a peer-initiated service by domain and validates ownership. +func (m *Manager) lookupPeerService(ctx context.Context, accountID, peerID, domain string) (*service.Service, error) { + svc, err := m.store.GetServiceByDomain(ctx, accountID, domain) + if err != nil { + return nil, err + } + + if svc.Source != service.SourceEphemeral { + return nil, status.Errorf(status.PermissionDenied, "cannot operate on API-created service via peer expose") + } + + if svc.SourcePeer != peerID { + return nil, status.Errorf(status.PermissionDenied, "cannot operate on service exposed by another peer") + } + + return svc, nil +} + +func (m *Manager) deletePeerService(ctx context.Context, accountID, peerID, serviceID string, activityCode activity.Activity) error { + var svc *service.Service + err := m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error { + var err error + svc, err = transaction.GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID) + if err != nil { + return err + } + + if svc.Source != service.SourceEphemeral { + return status.Errorf(status.PermissionDenied, "cannot delete API-created service via peer expose") + } + + if svc.SourcePeer != peerID { + return status.Errorf(status.PermissionDenied, "cannot delete service exposed by another peer") + } + + if err = transaction.DeleteService(ctx, accountID, serviceID); err != nil { + return fmt.Errorf("delete service: %w", err) + } + + return nil + }) + if err != nil { + return err + } + + peer, err := m.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID) + if err != nil { + log.WithContext(ctx).Debugf("failed to get peer %s for event metadata: %v", peerID, err) + peer = nil + } + + meta := addPeerInfoToEventMeta(svc.EventMeta(), peer) + + m.accountManager.StoreEvent(ctx, peerID, serviceID, accountID, activityCode, meta) + + m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Delete, "", m.proxyController.GetOIDCValidationConfig()), svc.ProxyCluster) + + m.accountManager.UpdateAccountPeers(ctx, accountID) + + return nil +} + +func addPeerInfoToEventMeta(meta map[string]any, peer *nbpeer.Peer) map[string]any { + if peer == nil { + return meta + } + meta["peer_name"] = peer.Name + if peer.IP != nil { + meta["peer_ip"] = peer.IP.String() + } + return meta +} diff --git a/management/internals/modules/reverseproxy/service/manager/manager_test.go b/management/internals/modules/reverseproxy/service/manager/manager_test.go index be61372d0..a205588ca 100644 --- a/management/internals/modules/reverseproxy/service/manager/manager_test.go +++ b/management/internals/modules/reverseproxy/service/manager/manager_test.go @@ -3,6 +3,7 @@ package manager import ( "context" "errors" + "net" "testing" "time" @@ -11,7 +12,13 @@ import ( "github.com/stretchr/testify/require" rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" + nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" + "github.com/netbirdio/netbird/management/server/activity" + "github.com/netbirdio/netbird/management/server/mock_server" + nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/store" + "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/shared/management/status" ) @@ -356,7 +363,7 @@ func TestPreserveServiceMetadata(t *testing.T) { existing := &rpservice.Service{ Meta: rpservice.ServiceMeta{ - CertificateIssuedAt: time.Now(), + CertificateIssuedAt: func() *time.Time { t := time.Now(); return &t }(), Status: "active", }, SessionPrivateKey: "private-key", @@ -373,3 +380,715 @@ func TestPreserveServiceMetadata(t *testing.T) { assert.Equal(t, existing.SessionPrivateKey, updated.SessionPrivateKey) assert.Equal(t, existing.SessionPublicKey, updated.SessionPublicKey) } + +func TestDeletePeerService_SourcePeerValidation(t *testing.T) { + ctx := context.Background() + accountID := "test-account" + ownerPeerID := "peer-owner" + otherPeerID := "peer-other" + serviceID := "service-123" + + testPeer := &nbpeer.Peer{ + ID: ownerPeerID, + Name: "test-peer", + IP: net.ParseIP("100.64.0.1"), + } + + newEphemeralService := func() *rpservice.Service { + return &rpservice.Service{ + ID: serviceID, + AccountID: accountID, + Name: "test-service", + Domain: "test.example.com", + Source: rpservice.SourceEphemeral, + SourcePeer: ownerPeerID, + } + } + + newPermanentService := func() *rpservice.Service { + return &rpservice.Service{ + ID: serviceID, + AccountID: accountID, + Name: "api-service", + Domain: "api.example.com", + Source: rpservice.SourcePermanent, + } + } + + newProxyServer := func(t *testing.T) *nbgrpc.ProxyServiceServer { + t.Helper() + tokenStore, err := nbgrpc.NewOneTimeTokenStore(context.Background(), 1*time.Hour, 10*time.Minute, 100) + require.NoError(t, err) + srv := nbgrpc.NewProxyServiceServer(nil, tokenStore, nbgrpc.ProxyOIDCConfig{}, nil, nil, nil) + t.Cleanup(srv.Close) + return srv + } + + t.Run("owner peer can delete own service", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var storedActivity activity.Activity + mockStore := store.NewMockStore(ctrl) + mockAccountMgr := &mock_server.MockAccountManager{ + StoreEventFunc: func(_ context.Context, _, _, _ string, activityID activity.ActivityDescriber, _ map[string]any) { + storedActivity = activityID.(activity.Activity) + }, + UpdateAccountPeersFunc: func(_ context.Context, _ string) {}, + } + + mockStore.EXPECT(). + ExecuteInTransaction(ctx, gomock.Any()). + DoAndReturn(func(ctx context.Context, fn func(store.Store) error) error { + txMock := store.NewMockStore(ctrl) + txMock.EXPECT(). + GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). + Return(newEphemeralService(), nil) + txMock.EXPECT(). + DeleteService(ctx, accountID, serviceID). + Return(nil) + return fn(txMock) + }) + mockStore.EXPECT(). + GetPeerByID(ctx, store.LockingStrengthNone, accountID, ownerPeerID). + Return(testPeer, nil) + + mgr := &Manager{ + store: mockStore, + accountManager: mockAccountMgr, + proxyController: NewGRPCProxyController(newProxyServer(t)), + } + + err := mgr.deletePeerService(ctx, accountID, ownerPeerID, serviceID, activity.PeerServiceUnexposed) + require.NoError(t, err) + assert.Equal(t, activity.PeerServiceUnexposed, storedActivity, "should store unexposed activity") + }) + + t.Run("different peer cannot delete service", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockStore(ctrl) + + mockStore.EXPECT(). + ExecuteInTransaction(ctx, gomock.Any()). + DoAndReturn(func(ctx context.Context, fn func(store.Store) error) error { + txMock := store.NewMockStore(ctrl) + txMock.EXPECT(). + GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). + Return(newEphemeralService(), nil) + return fn(txMock) + }) + + mgr := &Manager{ + store: mockStore, + } + + err := mgr.deletePeerService(ctx, accountID, otherPeerID, serviceID, activity.PeerServiceUnexposed) + require.Error(t, err) + + sErr, ok := status.FromError(err) + require.True(t, ok, "should be a status error") + assert.Equal(t, status.PermissionDenied, sErr.Type(), "should be permission denied") + assert.Contains(t, err.Error(), "another peer") + }) + + t.Run("cannot delete API-created service", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := store.NewMockStore(ctrl) + + mockStore.EXPECT(). + ExecuteInTransaction(ctx, gomock.Any()). + DoAndReturn(func(ctx context.Context, fn func(store.Store) error) error { + txMock := store.NewMockStore(ctrl) + txMock.EXPECT(). + GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). + Return(newPermanentService(), nil) + return fn(txMock) + }) + + mgr := &Manager{ + store: mockStore, + } + + err := mgr.deletePeerService(ctx, accountID, ownerPeerID, serviceID, activity.PeerServiceUnexposed) + require.Error(t, err) + + sErr, ok := status.FromError(err) + require.True(t, ok, "should be a status error") + assert.Equal(t, status.PermissionDenied, sErr.Type(), "should be permission denied") + assert.Contains(t, err.Error(), "API-created") + }) + + t.Run("expire uses correct activity code", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var storedActivity activity.Activity + mockStore := store.NewMockStore(ctrl) + mockAccountMgr := &mock_server.MockAccountManager{ + StoreEventFunc: func(_ context.Context, _, _, _ string, activityID activity.ActivityDescriber, _ map[string]any) { + storedActivity = activityID.(activity.Activity) + }, + UpdateAccountPeersFunc: func(_ context.Context, _ string) {}, + } + + mockStore.EXPECT(). + ExecuteInTransaction(ctx, gomock.Any()). + DoAndReturn(func(ctx context.Context, fn func(store.Store) error) error { + txMock := store.NewMockStore(ctrl) + txMock.EXPECT(). + GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). + Return(newEphemeralService(), nil) + txMock.EXPECT(). + DeleteService(ctx, accountID, serviceID). + Return(nil) + return fn(txMock) + }) + mockStore.EXPECT(). + GetPeerByID(ctx, store.LockingStrengthNone, accountID, ownerPeerID). + Return(testPeer, nil) + + mgr := &Manager{ + store: mockStore, + accountManager: mockAccountMgr, + proxyController: NewGRPCProxyController(newProxyServer(t)), + } + + err := mgr.deletePeerService(ctx, accountID, ownerPeerID, serviceID, activity.PeerServiceExposeExpired) + require.NoError(t, err) + assert.Equal(t, activity.PeerServiceExposeExpired, storedActivity, "should store expired activity") + }) + + t.Run("event meta includes peer info", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var storedMeta map[string]any + mockStore := store.NewMockStore(ctrl) + mockAccountMgr := &mock_server.MockAccountManager{ + StoreEventFunc: func(_ context.Context, _, _, _ string, _ activity.ActivityDescriber, meta map[string]any) { + storedMeta = meta + }, + UpdateAccountPeersFunc: func(_ context.Context, _ string) {}, + } + + mockStore.EXPECT(). + ExecuteInTransaction(ctx, gomock.Any()). + DoAndReturn(func(ctx context.Context, fn func(store.Store) error) error { + txMock := store.NewMockStore(ctrl) + txMock.EXPECT(). + GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). + Return(newEphemeralService(), nil) + txMock.EXPECT(). + DeleteService(ctx, accountID, serviceID). + Return(nil) + return fn(txMock) + }) + mockStore.EXPECT(). + GetPeerByID(ctx, store.LockingStrengthNone, accountID, ownerPeerID). + Return(testPeer, nil) + + mgr := &Manager{ + store: mockStore, + accountManager: mockAccountMgr, + proxyController: NewGRPCProxyController(newProxyServer(t)), + } + + err := mgr.deletePeerService(ctx, accountID, ownerPeerID, serviceID, activity.PeerServiceUnexposed) + require.NoError(t, err) + require.NotNil(t, storedMeta) + assert.Equal(t, "test-peer", storedMeta["peer_name"], "meta should contain peer name") + assert.Equal(t, "100.64.0.1", storedMeta["peer_ip"], "meta should contain peer IP") + assert.Equal(t, "test-service", storedMeta["name"], "meta should contain service name") + assert.Equal(t, "test.example.com", storedMeta["domain"], "meta should contain service domain") + }) +} + +// testClusterDeriver is a minimal ClusterDeriver that returns a fixed domain list. +type testClusterDeriver struct { + domains []string +} + +func (d *testClusterDeriver) DeriveClusterFromDomain(_ context.Context, _, domain string) (string, error) { + return "test-cluster", nil +} + +func (d *testClusterDeriver) GetClusterDomains() []string { + return d.domains +} + +const ( + testAccountID = "test-account" + testPeerID = "test-peer-1" + testGroupID = "test-group-1" + testUserID = "test-user" +) + +// setupIntegrationTest creates a real SQLite store with seeded test data for integration tests. +func setupIntegrationTest(t *testing.T) (*Manager, store.Store) { + t.Helper() + + ctx := context.Background() + testStore, cleanup, err := store.NewTestStoreFromSQL(ctx, "", t.TempDir()) + require.NoError(t, err) + t.Cleanup(cleanup) + + err = testStore.SaveAccount(ctx, &types.Account{ + Id: testAccountID, + CreatedBy: testUserID, + Settings: &types.Settings{ + PeerExposeEnabled: true, + PeerExposeGroups: []string{testGroupID}, + }, + Users: map[string]*types.User{ + testUserID: { + Id: testUserID, + AccountID: testAccountID, + Role: types.UserRoleAdmin, + }, + }, + Peers: map[string]*nbpeer.Peer{ + testPeerID: { + ID: testPeerID, + AccountID: testAccountID, + Key: "test-key", + DNSLabel: "test-peer", + Name: "test-peer", + IP: net.ParseIP("100.64.0.1"), + Status: &nbpeer.PeerStatus{Connected: true, LastSeen: time.Now()}, + Meta: nbpeer.PeerSystemMeta{Hostname: "test-peer"}, + }, + }, + Groups: map[string]*types.Group{ + testGroupID: { + ID: testGroupID, + AccountID: testAccountID, + Name: "Expose Group", + }, + }, + }) + require.NoError(t, err) + + err = testStore.AddPeerToGroup(ctx, testAccountID, testPeerID, testGroupID) + require.NoError(t, err) + + permsMgr := permissions.NewManager(testStore) + + accountMgr := &mock_server.MockAccountManager{ + StoreEventFunc: func(_ context.Context, _, _, _ string, _ activity.ActivityDescriber, _ map[string]any) {}, + UpdateAccountPeersFunc: func(_ context.Context, _ string) {}, + GetGroupByNameFunc: func(ctx context.Context, accountID, groupName string) (*types.Group, error) { + return testStore.GetGroupByName(ctx, store.LockingStrengthNone, groupName, accountID) + }, + } + + tokenStore, err := nbgrpc.NewOneTimeTokenStore(ctx, 1*time.Hour, 10*time.Minute, 100) + require.NoError(t, err) + proxySrv := nbgrpc.NewProxyServiceServer(nil, tokenStore, nbgrpc.ProxyOIDCConfig{}, nil, nil, nil) + t.Cleanup(proxySrv.Close) + + mgr := &Manager{ + store: testStore, + accountManager: accountMgr, + permissionsManager: permsMgr, + proxyController: NewGRPCProxyController(proxySrv), + clusterDeriver: &testClusterDeriver{ + domains: []string{"test.netbird.io"}, + }, + } + mgr.exposeTracker = &exposeTracker{manager: mgr} + + return mgr, testStore +} + +func Test_validateExposePermission(t *testing.T) { + ctx := context.Background() + + t.Run("allowed when peer is in expose group", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + err := mgr.validateExposePermission(ctx, testAccountID, testPeerID) + assert.NoError(t, err) + }) + + t.Run("denied when peer is not in expose group", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + // Add a peer that is NOT in the expose group + otherPeerID := "other-peer" + err := testStore.AddPeerToAccount(ctx, &nbpeer.Peer{ + ID: otherPeerID, + AccountID: testAccountID, + Key: "other-key", + DNSLabel: "other-peer", + Name: "other-peer", + IP: net.ParseIP("100.64.0.2"), + Status: &nbpeer.PeerStatus{LastSeen: time.Now()}, + Meta: nbpeer.PeerSystemMeta{Hostname: "other-peer"}, + }) + require.NoError(t, err) + + err = mgr.validateExposePermission(ctx, testAccountID, otherPeerID) + require.Error(t, err) + assert.Contains(t, err.Error(), "not in an allowed expose group") + }) + + t.Run("denied when expose is disabled", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + // Disable peer expose + s, err := testStore.GetAccountSettings(ctx, store.LockingStrengthNone, testAccountID) + require.NoError(t, err) + s.PeerExposeEnabled = false + err = testStore.SaveAccountSettings(ctx, testAccountID, s) + require.NoError(t, err) + + err = mgr.validateExposePermission(ctx, testAccountID, testPeerID) + require.Error(t, err) + assert.Contains(t, err.Error(), "not enabled") + }) + + t.Run("disallowed when no groups configured", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + // Enable expose with empty groups — no groups configured means no peer is allowed + s, err := testStore.GetAccountSettings(ctx, store.LockingStrengthNone, testAccountID) + require.NoError(t, err) + s.PeerExposeGroups = []string{} + err = testStore.SaveAccountSettings(ctx, testAccountID, s) + require.NoError(t, err) + + err = mgr.validateExposePermission(ctx, testAccountID, testPeerID) + assert.Error(t, err) + }) + + t.Run("error when store returns error", func(t *testing.T) { + ctrl := gomock.NewController(t) + mockStore := store.NewMockStore(ctrl) + mockStore.EXPECT().GetAccountSettings(gomock.Any(), gomock.Any(), testAccountID).Return(nil, errors.New("store error")) + mgr := &Manager{store: mockStore} + err := mgr.validateExposePermission(ctx, testAccountID, testPeerID) + require.Error(t, err) + assert.Contains(t, err.Error(), "get account settings") + }) +} + +func TestCreateServiceFromPeer(t *testing.T) { + ctx := context.Background() + + t.Run("creates service with random domain", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + req := &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + } + + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.NoError(t, err) + assert.NotEmpty(t, resp.ServiceName, "service name should be generated") + assert.Contains(t, resp.Domain, "test.netbird.io", "domain should use cluster domain") + assert.NotEmpty(t, resp.ServiceURL, "service URL should be set") + + // Verify service is persisted in store + persisted, err := testStore.GetServiceByDomain(ctx, testAccountID, resp.Domain) + require.NoError(t, err) + assert.Equal(t, resp.Domain, persisted.Domain) + assert.Equal(t, rpservice.SourceEphemeral, persisted.Source, "source should be ephemeral") + assert.Equal(t, testPeerID, persisted.SourcePeer, "source peer should be set") + assert.NotNil(t, persisted.Meta.LastRenewedAt, "last renewed should be set") + }) + + t.Run("creates service with custom domain", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + + req := &rpservice.ExposeServiceRequest{ + Port: 80, + Protocol: "http", + Domain: "example.com", + } + + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.NoError(t, err) + assert.Contains(t, resp.Domain, "example.com", "should use the provided domain") + }) + + t.Run("validates expose permission internally", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + // Disable peer expose + s, err := testStore.GetAccountSettings(ctx, store.LockingStrengthNone, testAccountID) + require.NoError(t, err) + s.PeerExposeEnabled = false + err = testStore.SaveAccountSettings(ctx, testAccountID, s) + require.NoError(t, err) + + req := &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + } + + _, err = mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.Error(t, err) + assert.Contains(t, err.Error(), "not enabled") + }) + + t.Run("validates request fields", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + + req := &rpservice.ExposeServiceRequest{ + Port: 0, + Protocol: "http", + } + + _, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.Error(t, err) + assert.Contains(t, err.Error(), "port") + }) +} + +func TestExposeServiceRequestValidate(t *testing.T) { + tests := []struct { + name string + req rpservice.ExposeServiceRequest + wantErr string + }{ + { + name: "valid http request", + req: rpservice.ExposeServiceRequest{Port: 8080, Protocol: "http"}, + wantErr: "", + }, + { + name: "valid https request with pin", + req: rpservice.ExposeServiceRequest{Port: 443, Protocol: "https", Pin: "123456"}, + wantErr: "", + }, + { + name: "port zero rejected", + req: rpservice.ExposeServiceRequest{Port: 0, Protocol: "http"}, + wantErr: "port must be between 1 and 65535", + }, + { + name: "negative port rejected", + req: rpservice.ExposeServiceRequest{Port: -1, Protocol: "http"}, + wantErr: "port must be between 1 and 65535", + }, + { + name: "port above 65535 rejected", + req: rpservice.ExposeServiceRequest{Port: 65536, Protocol: "http"}, + wantErr: "port must be between 1 and 65535", + }, + { + name: "unsupported protocol", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "tcp"}, + wantErr: "unsupported protocol", + }, + { + name: "invalid pin format", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "http", Pin: "abc"}, + wantErr: "invalid pin", + }, + { + name: "pin too short", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "http", Pin: "12345"}, + wantErr: "invalid pin", + }, + { + name: "valid 6-digit pin", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "http", Pin: "000000"}, + wantErr: "", + }, + { + name: "empty user group name", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "http", UserGroups: []string{"valid", ""}}, + wantErr: "user group name cannot be empty", + }, + { + name: "invalid name prefix", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "http", NamePrefix: "INVALID"}, + wantErr: "invalid name prefix", + }, + { + name: "valid name prefix", + req: rpservice.ExposeServiceRequest{Port: 80, Protocol: "http", NamePrefix: "my-service"}, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.req.Validate() + if tt.wantErr == "" { + assert.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + } + }) + } + + t.Run("nil receiver", func(t *testing.T) { + var req *rpservice.ExposeServiceRequest + err := req.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "request cannot be nil") + }) +} + +func TestDeleteServiceFromPeer_ByDomain(t *testing.T) { + ctx := context.Background() + + t.Run("deletes service by domain", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + // First create a service + req := &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + } + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.NoError(t, err) + + // Delete by domain using unexported method + err = mgr.deleteServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain, false) + require.NoError(t, err) + + // Verify service is deleted + _, err = testStore.GetServiceByDomain(ctx, testAccountID, resp.Domain) + require.Error(t, err, "service should be deleted") + }) + + t.Run("expire uses correct activity", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + + req := &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + } + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.NoError(t, err) + + err = mgr.deleteServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain, true) + require.NoError(t, err) + }) +} + +func TestStopServiceFromPeer(t *testing.T) { + ctx := context.Background() + + t.Run("stops service by domain", func(t *testing.T) { + mgr, testStore := setupIntegrationTest(t) + + req := &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + } + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, req) + require.NoError(t, err) + + err = mgr.StopServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain) + require.NoError(t, err) + + _, err = testStore.GetServiceByDomain(ctx, testAccountID, resp.Domain) + require.Error(t, err, "service should be deleted") + }) +} + +func TestDeleteService_UntracksEphemeralExpose(t *testing.T) { + ctx := context.Background() + mgr, _ := setupIntegrationTest(t) + + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + }) + require.NoError(t, err) + assert.Equal(t, 1, mgr.exposeTracker.CountPeerExposes(testPeerID), "expose should be tracked after create") + + // Look up the service by domain to get its store ID + svc, err := mgr.store.GetServiceByDomain(ctx, testAccountID, resp.Domain) + require.NoError(t, err) + + // Delete via the API path (user-initiated) + err = mgr.DeleteService(ctx, testAccountID, testUserID, svc.ID) + require.NoError(t, err) + + assert.Equal(t, 0, mgr.exposeTracker.CountPeerExposes(testPeerID), "expose should be untracked after API delete") + + // A new expose should succeed (not blocked by stale tracking) + _, err = mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 9090, + Protocol: "http", + }) + assert.NoError(t, err, "new expose should succeed after API delete cleared tracking") +} + +func TestDeleteAllServices_UntracksEphemeralExposes(t *testing.T) { + ctx := context.Background() + mgr, _ := setupIntegrationTest(t) + + for i := range 3 { + _, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080 + i, + Protocol: "http", + }) + require.NoError(t, err) + } + + assert.Equal(t, 3, mgr.exposeTracker.CountPeerExposes(testPeerID), "all exposes should be tracked") + + err := mgr.DeleteAllServices(ctx, testAccountID, testUserID) + require.NoError(t, err) + + assert.Equal(t, 0, mgr.exposeTracker.CountPeerExposes(testPeerID), "all exposes should be untracked after DeleteAllServices") +} + +func TestRenewServiceFromPeer(t *testing.T) { + ctx := context.Background() + + t.Run("renews tracked expose", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + }) + require.NoError(t, err) + + err = mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain) + require.NoError(t, err) + }) + + t.Run("fails for untracked domain", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + err := mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, "nonexistent.com") + require.Error(t, err) + }) +} + +func TestGetGroupIDsFromNames(t *testing.T) { + ctx := context.Background() + + t.Run("resolves group names to IDs", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + ids, err := mgr.getGroupIDsFromNames(ctx, testAccountID, []string{"Expose Group"}) + require.NoError(t, err) + require.Len(t, ids, 1, "should return exactly one group ID") + assert.Equal(t, testGroupID, ids[0]) + }) + + t.Run("returns error for unknown group", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + _, err := mgr.getGroupIDsFromNames(ctx, testAccountID, []string{"nonexistent"}) + require.Error(t, err) + }) + + t.Run("returns error for empty group list", func(t *testing.T) { + mgr, _ := setupIntegrationTest(t) + _, err := mgr.getGroupIDsFromNames(ctx, testAccountID, []string{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "no group names provided") + }) +} diff --git a/management/internals/modules/reverseproxy/service/service.go b/management/internals/modules/reverseproxy/service/service.go index 40d40c2ee..0554beda9 100644 --- a/management/internals/modules/reverseproxy/service/service.go +++ b/management/internals/modules/reverseproxy/service/service.go @@ -1,10 +1,13 @@ package service import ( + "crypto/rand" "errors" "fmt" + "math/big" "net" "net/url" + "regexp" "strconv" "time" @@ -40,6 +43,9 @@ const ( TargetTypeHost = "host" TargetTypeDomain = "domain" TargetTypeSubnet = "subnet" + + SourcePermanent = "permanent" + SourceEphemeral = "ephemeral" ) type Target struct { @@ -114,8 +120,9 @@ type OIDCValidationConfig struct { type ServiceMeta struct { CreatedAt time.Time - CertificateIssuedAt time.Time + CertificateIssuedAt *time.Time Status string + LastRenewedAt *time.Time } type Service struct { @@ -132,6 +139,8 @@ type Service struct { Meta ServiceMeta `gorm:"embedded;embeddedPrefix:meta_"` SessionPrivateKey string `gorm:"column:session_private_key"` SessionPublicKey string `gorm:"column:session_public_key"` + Source string `gorm:"default:'permanent'"` + SourcePeer string } func NewService(accountID, name, domain, proxyCluster string, targets []*Target, enabled bool) *Service { @@ -207,8 +216,8 @@ func (s *Service) ToAPIResponse() *api.Service { Status: api.ServiceMetaStatus(s.Meta.Status), } - if !s.Meta.CertificateIssuedAt.IsZero() { - meta.CertificateIssuedAt = &s.Meta.CertificateIssuedAt + if s.Meta.CertificateIssuedAt != nil { + meta.CertificateIssuedAt = s.Meta.CertificateIssuedAt } resp := &api.Service{ @@ -403,7 +412,11 @@ func (s *Service) Validate() error { } func (s *Service) EventMeta() map[string]any { - return map[string]any{"name": s.Name, "domain": s.Domain, "proxy_cluster": s.ProxyCluster} + return map[string]any{"name": s.Name, "domain": s.Domain, "proxy_cluster": s.ProxyCluster, "source": s.Source, "auth": s.isAuthEnabled()} +} + +func (s *Service) isAuthEnabled() bool { + return s.Auth.PasswordAuth != nil || s.Auth.PinAuth != nil || s.Auth.BearerAuth != nil } func (s *Service) Copy() *Service { @@ -427,6 +440,8 @@ func (s *Service) Copy() *Service { Meta: s.Meta, SessionPrivateKey: s.SessionPrivateKey, SessionPublicKey: s.SessionPublicKey, + Source: s.Source, + SourcePeer: s.SourcePeer, } } @@ -461,3 +476,140 @@ func (s *Service) DecryptSensitiveData(enc *crypt.FieldEncrypt) error { return nil } + +var pinRegexp = regexp.MustCompile(`^\d{6}$`) + +const alphanumCharset = "abcdefghijklmnopqrstuvwxyz0123456789" + +var validNamePrefix = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$`) + +// ExposeServiceRequest contains the parameters for creating a peer-initiated expose service. +type ExposeServiceRequest struct { + NamePrefix string + Port int + Protocol string + Domain string + Pin string + Password string + UserGroups []string +} + +// Validate checks all fields of the expose request. +func (r *ExposeServiceRequest) Validate() error { + if r == nil { + return errors.New("request cannot be nil") + } + + if r.Port < 1 || r.Port > 65535 { + return fmt.Errorf("port must be between 1 and 65535, got %d", r.Port) + } + + if r.Protocol != "http" && r.Protocol != "https" { + return fmt.Errorf("unsupported protocol %q: must be http or https", r.Protocol) + } + + if r.Pin != "" && !pinRegexp.MatchString(r.Pin) { + return errors.New("invalid pin: must be exactly 6 digits") + } + + for _, g := range r.UserGroups { + if g == "" { + return errors.New("user group name cannot be empty") + } + } + + if r.NamePrefix != "" && !validNamePrefix.MatchString(r.NamePrefix) { + return fmt.Errorf("invalid name prefix %q: must be lowercase alphanumeric with optional hyphens, 1-32 characters", r.NamePrefix) + } + + return nil +} + +// ToService builds a Service from the expose request. +func (r *ExposeServiceRequest) ToService(accountID, peerID, serviceName string) *Service { + service := &Service{ + AccountID: accountID, + Name: serviceName, + Enabled: true, + Targets: []*Target{ + { + AccountID: accountID, + Port: r.Port, + Protocol: r.Protocol, + TargetId: peerID, + TargetType: TargetTypePeer, + Enabled: true, + }, + }, + } + + if r.Domain != "" { + service.Domain = serviceName + "." + r.Domain + } + + if r.Pin != "" { + service.Auth.PinAuth = &PINAuthConfig{ + Enabled: true, + Pin: r.Pin, + } + } + + if r.Password != "" { + service.Auth.PasswordAuth = &PasswordAuthConfig{ + Enabled: true, + Password: r.Password, + } + } + + if len(r.UserGroups) > 0 { + service.Auth.BearerAuth = &BearerAuthConfig{ + Enabled: true, + DistributionGroups: r.UserGroups, + } + } + + return service +} + +// ExposeServiceResponse contains the result of a successful peer expose creation. +type ExposeServiceResponse struct { + ServiceName string + ServiceURL string + Domain string +} + +// GenerateExposeName generates a random service name for peer-exposed services. +// The prefix, if provided, must be a valid DNS label component (lowercase alphanumeric and hyphens). +func GenerateExposeName(prefix string) (string, error) { + if prefix != "" && !validNamePrefix.MatchString(prefix) { + return "", fmt.Errorf("invalid name prefix %q: must be lowercase alphanumeric with optional hyphens, 1-32 characters", prefix) + } + + suffixLen := 12 + if prefix != "" { + suffixLen = 4 + } + + suffix, err := randomAlphanumeric(suffixLen) + if err != nil { + return "", fmt.Errorf("generate random name: %w", err) + } + + if prefix == "" { + return suffix, nil + } + return prefix + "-" + suffix, nil +} + +func randomAlphanumeric(n int) (string, error) { + result := make([]byte, n) + charsetLen := big.NewInt(int64(len(alphanumCharset))) + for i := range result { + idx, err := rand.Int(rand.Reader, charsetLen) + if err != nil { + return "", err + } + result[i] = alphanumCharset[idx.Int64()] + } + return string(result), nil +} diff --git a/management/internals/modules/reverseproxy/service/service_test.go b/management/internals/modules/reverseproxy/service/service_test.go index ac1dd02a4..4d526c930 100644 --- a/management/internals/modules/reverseproxy/service/service_test.go +++ b/management/internals/modules/reverseproxy/service/service_test.go @@ -403,3 +403,146 @@ func TestAuthConfig_ClearSecrets(t *testing.T) { t.Errorf("PIN not cleared, got: %s", config.PinAuth.Pin) } } + +func TestGenerateExposeName(t *testing.T) { + t.Run("no prefix generates 12-char name", func(t *testing.T) { + name, err := GenerateExposeName("") + require.NoError(t, err) + assert.Len(t, name, 12) + assert.Regexp(t, `^[a-z0-9]+$`, name) + }) + + t.Run("with prefix generates prefix-XXXX", func(t *testing.T) { + name, err := GenerateExposeName("myapp") + require.NoError(t, err) + assert.True(t, strings.HasPrefix(name, "myapp-"), "name should start with prefix") + suffix := strings.TrimPrefix(name, "myapp-") + assert.Len(t, suffix, 4, "suffix should be 4 chars") + assert.Regexp(t, `^[a-z0-9]+$`, suffix) + }) + + t.Run("unique names", func(t *testing.T) { + names := make(map[string]bool) + for i := 0; i < 50; i++ { + name, err := GenerateExposeName("") + require.NoError(t, err) + names[name] = true + } + assert.Greater(t, len(names), 45, "should generate mostly unique names") + }) + + t.Run("valid prefixes", func(t *testing.T) { + validPrefixes := []string{"a", "ab", "a1", "my-app", "web-server-01", "a-b"} + for _, prefix := range validPrefixes { + name, err := GenerateExposeName(prefix) + assert.NoError(t, err, "prefix %q should be valid", prefix) + assert.True(t, strings.HasPrefix(name, prefix+"-"), "name should start with %q-", prefix) + } + }) + + t.Run("invalid prefixes", func(t *testing.T) { + invalidPrefixes := []string{ + "-starts-with-dash", + "ends-with-dash-", + "has.dots", + "HAS-UPPER", + "has spaces", + "has/slash", + "a--", + } + for _, prefix := range invalidPrefixes { + _, err := GenerateExposeName(prefix) + assert.Error(t, err, "prefix %q should be invalid", prefix) + assert.Contains(t, err.Error(), "invalid name prefix") + } + }) +} + +func TestExposeServiceRequest_ToService(t *testing.T) { + t.Run("basic HTTP service", func(t *testing.T) { + req := &ExposeServiceRequest{ + Port: 8080, + Protocol: "http", + } + + service := req.ToService("account-1", "peer-1", "mysvc") + + assert.Equal(t, "account-1", service.AccountID) + assert.Equal(t, "mysvc", service.Name) + assert.True(t, service.Enabled) + assert.Empty(t, service.Domain, "domain should be empty when not specified") + require.Len(t, service.Targets, 1) + + target := service.Targets[0] + assert.Equal(t, 8080, target.Port) + assert.Equal(t, "http", target.Protocol) + assert.Equal(t, "peer-1", target.TargetId) + assert.Equal(t, TargetTypePeer, target.TargetType) + assert.True(t, target.Enabled) + assert.Equal(t, "account-1", target.AccountID) + }) + + t.Run("with custom domain", func(t *testing.T) { + req := &ExposeServiceRequest{ + Port: 3000, + Domain: "example.com", + } + + service := req.ToService("acc", "peer", "web") + assert.Equal(t, "web.example.com", service.Domain) + }) + + t.Run("with PIN auth", func(t *testing.T) { + req := &ExposeServiceRequest{ + Port: 80, + Pin: "1234", + } + + service := req.ToService("acc", "peer", "svc") + require.NotNil(t, service.Auth.PinAuth) + assert.True(t, service.Auth.PinAuth.Enabled) + assert.Equal(t, "1234", service.Auth.PinAuth.Pin) + assert.Nil(t, service.Auth.PasswordAuth) + assert.Nil(t, service.Auth.BearerAuth) + }) + + t.Run("with password auth", func(t *testing.T) { + req := &ExposeServiceRequest{ + Port: 80, + Password: "secret", + } + + service := req.ToService("acc", "peer", "svc") + require.NotNil(t, service.Auth.PasswordAuth) + assert.True(t, service.Auth.PasswordAuth.Enabled) + assert.Equal(t, "secret", service.Auth.PasswordAuth.Password) + }) + + t.Run("with user groups (bearer auth)", func(t *testing.T) { + req := &ExposeServiceRequest{ + Port: 80, + UserGroups: []string{"admins", "devs"}, + } + + service := req.ToService("acc", "peer", "svc") + require.NotNil(t, service.Auth.BearerAuth) + assert.True(t, service.Auth.BearerAuth.Enabled) + assert.Equal(t, []string{"admins", "devs"}, service.Auth.BearerAuth.DistributionGroups) + }) + + t.Run("with all auth types", func(t *testing.T) { + req := &ExposeServiceRequest{ + Port: 443, + Domain: "myco.com", + Pin: "9999", + Password: "pass", + UserGroups: []string{"ops"}, + } + + service := req.ToService("acc", "peer", "full") + assert.Equal(t, "full.myco.com", service.Domain) + require.NotNil(t, service.Auth.PinAuth) + require.NotNil(t, service.Auth.PasswordAuth) + require.NotNil(t, service.Auth.BearerAuth) + }) +} diff --git a/management/internals/server/boot.go b/management/internals/server/boot.go index 0407a7678..2049f0051 100644 --- a/management/internals/server/boot.go +++ b/management/internals/server/boot.go @@ -152,6 +152,11 @@ func (s *BaseServer) GRPCServer() *grpc.Server { if err != nil { log.Fatalf("failed to create management server: %v", err) } + serviceMgr := s.ServiceManager() + srv.SetReverseProxyManager(serviceMgr) + if serviceMgr != nil { + serviceMgr.StartExposeReaper(context.Background()) + } mgmtProto.RegisterManagementServiceServer(gRPCAPIHandler, srv) mgmtProto.RegisterProxyServiceServer(gRPCAPIHandler, s.ReverseProxyGRPCServer()) diff --git a/management/internals/shared/grpc/expose_service.go b/management/internals/shared/grpc/expose_service.go new file mode 100644 index 000000000..c444471b0 --- /dev/null +++ b/management/internals/shared/grpc/expose_service.go @@ -0,0 +1,192 @@ +package grpc + +import ( + "context" + + 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") + } + + created, err := reverseProxyMgr.CreateServiceFromPeer(ctx, accountID, peer.ID, &rpservice.ExposeServiceRequest{ + NamePrefix: exposeReq.NamePrefix, + Port: int(exposeReq.Port), + Protocol: exposeProtocolToString(exposeReq.Protocol), + Domain: exposeReq.Domain, + Pin: exposeReq.Pin, + Password: exposeReq.Password, + UserGroups: exposeReq.UserGroups, + }) + if err != nil { + return nil, mapExposeError(ctx, err) + } + + return s.encryptResponse(peerKey, &proto.ExposeServiceResponse{ + ServiceName: created.ServiceName, + ServiceUrl: created.ServiceURL, + Domain: created.Domain, + }) +} + +// 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") + } + + if err := reverseProxyMgr.RenewServiceFromPeer(ctx, accountID, peer.ID, renewReq.Domain); 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") + } + + if err := reverseProxyMgr.StopServiceFromPeer(ctx, accountID, peer.ID, stopReq.Domain); 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 +} + +func exposeProtocolToString(p proto.ExposeProtocol) string { + switch p { + case proto.ExposeProtocol_EXPOSE_HTTP: + return "http" + case proto.ExposeProtocol_EXPOSE_HTTPS: + return "https" + default: + return "http" + } +} diff --git a/management/internals/shared/grpc/proxy_group_access_test.go b/management/internals/shared/grpc/proxy_group_access_test.go index f86a5bb0c..22fe4506b 100644 --- a/management/internals/shared/grpc/proxy_group_access_test.go +++ b/management/internals/shared/grpc/proxy_group_access_test.go @@ -76,6 +76,20 @@ func (m *mockReverseProxyManager) GetServiceIDByTargetID(_ context.Context, _, _ return "", nil } +func (m *mockReverseProxyManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *service.ExposeServiceRequest) (*service.ExposeServiceResponse, error) { + return &service.ExposeServiceResponse{}, nil +} + +func (m *mockReverseProxyManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *mockReverseProxyManager) StopServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *mockReverseProxyManager) StartExposeReaper(_ context.Context) {} + type mockUsersManager struct { users map[string]*types.User err error diff --git a/management/internals/shared/grpc/proxy_test.go b/management/internals/shared/grpc/proxy_test.go index 8af9a7b16..3338afbd2 100644 --- a/management/internals/shared/grpc/proxy_test.go +++ b/management/internals/shared/grpc/proxy_test.go @@ -8,12 +8,66 @@ import ( "testing" "time" + "sync" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" "github.com/netbirdio/netbird/shared/management/proto" ) +type testProxyController struct { + mu sync.Mutex + clusterProxies map[string]map[string]struct{} +} + +func newTestProxyController() *testProxyController { + return &testProxyController{ + clusterProxies: make(map[string]map[string]struct{}), + } +} + +func (c *testProxyController) SendServiceUpdateToCluster(_ context.Context, _ string, _ *proto.ProxyMapping, _ string) { +} + +func (c *testProxyController) GetOIDCValidationConfig() rpservice.OIDCValidationConfig { + return rpservice.OIDCValidationConfig{} +} + +func (c *testProxyController) RegisterProxyToCluster(_ context.Context, clusterAddr, proxyID string) error { + c.mu.Lock() + defer c.mu.Unlock() + if _, ok := c.clusterProxies[clusterAddr]; !ok { + c.clusterProxies[clusterAddr] = make(map[string]struct{}) + } + c.clusterProxies[clusterAddr][proxyID] = struct{}{} + return nil +} + +func (c *testProxyController) UnregisterProxyFromCluster(_ context.Context, clusterAddr, proxyID string) error { + c.mu.Lock() + defer c.mu.Unlock() + if proxies, ok := c.clusterProxies[clusterAddr]; ok { + delete(proxies, proxyID) + } + return nil +} + +func (c *testProxyController) GetProxiesForCluster(clusterAddr string) []string { + c.mu.Lock() + defer c.mu.Unlock() + proxies, ok := c.clusterProxies[clusterAddr] + if !ok { + return nil + } + result := make([]string, 0, len(proxies)) + for id := range proxies { + result = append(result, id) + } + return result +} + // registerFakeProxy adds a fake proxy connection to the server's internal maps // and returns the channel where messages will be received. func registerFakeProxy(s *ProxyServiceServer, proxyID, clusterAddr string) chan *proto.GetMappingUpdateResponse { @@ -46,6 +100,7 @@ func TestSendServiceUpdateToCluster_UniqueTokensPerProxy(t *testing.T) { s := &ProxyServiceServer{ tokenStore: tokenStore, } + s.SetProxyController(newTestProxyController()) const cluster = "proxy.example.com" const numProxies = 3 @@ -102,6 +157,7 @@ func TestSendServiceUpdateToCluster_DeleteNoToken(t *testing.T) { s := &ProxyServiceServer{ tokenStore: tokenStore, } + s.SetProxyController(newTestProxyController()) const cluster = "proxy.example.com" ch1 := registerFakeProxy(s, "proxy-a", cluster) @@ -135,6 +191,7 @@ func TestSendServiceUpdate_UniqueTokensPerProxy(t *testing.T) { s := &ProxyServiceServer{ tokenStore: tokenStore, } + s.SetProxyController(newTestProxyController()) // Register proxies in different clusters (SendServiceUpdate broadcasts to all) ch1 := registerFakeProxy(s, "proxy-a", "cluster-a") diff --git a/management/internals/shared/grpc/server.go b/management/internals/shared/grpc/server.go index 0167aca07..a07cafe90 100644 --- a/management/internals/shared/grpc/server.go +++ b/management/internals/shared/grpc/server.go @@ -26,6 +26,7 @@ import ( "github.com/netbirdio/netbird/shared/management/client/common" "github.com/netbirdio/netbird/management/internals/controllers/network_map" + rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" nbconfig "github.com/netbirdio/netbird/management/internals/server/config" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/job" @@ -80,6 +81,9 @@ type Server struct { syncSem atomic.Int32 syncLimEnabled bool syncLim int32 + + reverseProxyManager rpservice.Manager + reverseProxyMu sync.RWMutex } // NewServer creates a new Management server diff --git a/management/internals/shared/grpc/validate_session_test.go b/management/internals/shared/grpc/validate_session_test.go index a0dfc9587..124ddf620 100644 --- a/management/internals/shared/grpc/validate_session_test.go +++ b/management/internals/shared/grpc/validate_session_test.go @@ -303,6 +303,20 @@ func (m *testValidateSessionServiceManager) GetServiceIDByTargetID(_ context.Con return "", nil } +func (m *testValidateSessionServiceManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *service.ExposeServiceRequest) (*service.ExposeServiceResponse, error) { + return nil, nil +} + +func (m *testValidateSessionServiceManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *testValidateSessionServiceManager) StopServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *testValidateSessionServiceManager) StartExposeReaper(_ context.Context) {} + type testValidateSessionProxyManager struct{} func (m *testValidateSessionProxyManager) Connect(_ context.Context, _, _, _ string) error { diff --git a/management/server/account.go b/management/server/account.go index 86e110b76..550971337 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -376,6 +376,7 @@ func (am *DefaultAccountManager) UpdateAccountSettings(ctx context.Context, acco am.handlePeerLoginExpirationSettings(ctx, oldSettings, newSettings, userID, accountID) am.handleGroupsPropagationSettings(ctx, oldSettings, newSettings, userID, accountID) am.handleAutoUpdateVersionSettings(ctx, oldSettings, newSettings, userID, accountID) + am.handlePeerExposeSettings(ctx, oldSettings, newSettings, userID, accountID) if err = am.handleInactivityExpirationSettings(ctx, oldSettings, newSettings, userID, accountID); err != nil { return nil, err } @@ -492,6 +493,21 @@ func (am *DefaultAccountManager) handleAutoUpdateVersionSettings(ctx context.Con } } +func (am *DefaultAccountManager) handlePeerExposeSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) { + oldEnabled := oldSettings.PeerExposeEnabled + newEnabled := newSettings.PeerExposeEnabled + + if oldEnabled == newEnabled { + return + } + + event := activity.AccountPeerExposeEnabled + if !newEnabled { + event = activity.AccountPeerExposeDisabled + } + am.StoreEvent(ctx, userID, accountID, accountID, event, nil) +} + func (am *DefaultAccountManager) handleInactivityExpirationSettings(ctx context.Context, oldSettings, newSettings *types.Settings, userID, accountID string) error { if newSettings.PeerInactivityExpirationEnabled { if oldSettings.PeerInactivityExpiration != newSettings.PeerInactivityExpiration { diff --git a/management/server/account_test.go b/management/server/account_test.go index cf096c519..8fea2f6f7 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -3124,7 +3124,7 @@ func createManager(t testing.TB) (*DefaultAccountManager, *update_channel.PeersU } proxyGrpcServer := nbgrpc.NewProxyServiceServer(nil, nil, nbgrpc.ProxyOIDCConfig{}, peersManager, nil) - manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, proxyGrpcServer, nil)) + manager.SetServiceManager(reverseproxymanager.NewManager(store, manager, permissionsManager, settingsMockManager, proxyGrpcServer, nil)) return manager, updateManager, nil } diff --git a/management/server/activity/codes.go b/management/server/activity/codes.go index e1b7e5300..53cf30d4c 100644 --- a/management/server/activity/codes.go +++ b/management/server/activity/codes.go @@ -208,6 +208,18 @@ const ( ServiceUpdated Activity = 109 ServiceDeleted Activity = 110 + // PeerServiceExposed indicates that a peer exposed a service via the reverse proxy + PeerServiceExposed Activity = 111 + // PeerServiceUnexposed indicates that a peer-exposed service was removed + PeerServiceUnexposed Activity = 112 + // PeerServiceExposeExpired indicates that a peer-exposed service was removed due to TTL expiration + PeerServiceExposeExpired Activity = 113 + + // AccountPeerExposeEnabled indicates that a user enabled peer expose for the account + AccountPeerExposeEnabled Activity = 114 + // AccountPeerExposeDisabled indicates that a user disabled peer expose for the account + AccountPeerExposeDisabled Activity = 115 + AccountDeleted Activity = 99999 ) @@ -345,6 +357,13 @@ var activityMap = map[Activity]Code{ ServiceCreated: {"Service created", "service.create"}, ServiceUpdated: {"Service updated", "service.update"}, ServiceDeleted: {"Service deleted", "service.delete"}, + + PeerServiceExposed: {"Peer exposed service", "service.peer.expose"}, + PeerServiceUnexposed: {"Peer unexposed service", "service.peer.unexpose"}, + PeerServiceExposeExpired: {"Peer exposed service expired", "service.peer.expose.expire"}, + + AccountPeerExposeEnabled: {"Account peer expose enabled", "account.setting.peer.expose.enable"}, + AccountPeerExposeDisabled: {"Account peer expose disabled", "account.setting.peer.expose.disable"}, } // StringCode returns a string code of the activity diff --git a/management/server/http/handlers/accounts/accounts_handler.go b/management/server/http/handlers/accounts/accounts_handler.go index 122c061ce..27a57c434 100644 --- a/management/server/http/handlers/accounts/accounts_handler.go +++ b/management/server/http/handlers/accounts/accounts_handler.go @@ -168,6 +168,10 @@ func (h *handler) getAllAccounts(w http.ResponseWriter, r *http.Request) { } func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJSONRequestBody) (*types.Settings, error) { + if req.Settings.PeerExposeEnabled && len(req.Settings.PeerExposeGroups) == 0 { + return nil, status.Errorf(status.InvalidArgument, "peer expose requires at least one group") + } + returnSettings := &types.Settings{ PeerLoginExpirationEnabled: req.Settings.PeerLoginExpirationEnabled, PeerLoginExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerLoginExpiration)), @@ -175,6 +179,9 @@ func (h *handler) updateAccountRequestSettings(req api.PutApiAccountsAccountIdJS PeerInactivityExpirationEnabled: req.Settings.PeerInactivityExpirationEnabled, PeerInactivityExpiration: time.Duration(float64(time.Second.Nanoseconds()) * float64(req.Settings.PeerInactivityExpiration)), + + PeerExposeEnabled: req.Settings.PeerExposeEnabled, + PeerExposeGroups: req.Settings.PeerExposeGroups, } if req.Settings.Extra != nil { @@ -336,6 +343,8 @@ func toAccountResponse(accountID string, settings *types.Settings, meta *types.A JwtAllowGroups: &jwtAllowGroups, RegularUsersViewBlocked: settings.RegularUsersViewBlocked, RoutingPeerDnsResolutionEnabled: &settings.RoutingPeerDNSResolutionEnabled, + PeerExposeEnabled: settings.PeerExposeEnabled, + PeerExposeGroups: settings.PeerExposeGroups, LazyConnectionEnabled: &settings.LazyConnectionEnabled, DnsDomain: &settings.DNSDomain, AutoUpdateVersion: &settings.AutoUpdateVersion, diff --git a/management/server/http/handlers/proxy/auth_callback_integration_test.go b/management/server/http/handlers/proxy/auth_callback_integration_test.go index c38e952c7..8e1a6ab28 100644 --- a/management/server/http/handlers/proxy/auth_callback_integration_test.go +++ b/management/server/http/handlers/proxy/auth_callback_integration_test.go @@ -414,6 +414,20 @@ func (m *testServiceManager) GetServiceIDByTargetID(_ context.Context, _, _ stri return "", nil } +func (m *testServiceManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *reverseproxy.ExposeServiceRequest) (*reverseproxy.ExposeServiceResponse, error) { + return nil, nil +} + +func (m *testServiceManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *testServiceManager) StopServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *testServiceManager) StartExposeReaper(_ context.Context) {} + func createTestState(t *testing.T, ps *nbgrpc.ProxyServiceServer, redirectURL string) string { t.Helper() diff --git a/management/server/http/testing/testing_tools/channel/channel.go b/management/server/http/testing/testing_tools/channel/channel.go index 9c6154a44..73c51d60b 100644 --- a/management/server/http/testing/testing_tools/channel/channel.go +++ b/management/server/http/testing/testing_tools/channel/channel.go @@ -99,7 +99,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee proxyMgr := proxymanager.NewManager(store) proxyServiceServer := nbgrpc.NewProxyServiceServer(accessLogsManager, proxyTokenStore, nbgrpc.ProxyOIDCConfig{}, peersManager, userManager, proxyMgr) domainManager := manager.NewManager(store, proxyMgr, permissionsManager) - serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, proxyServiceServer, domainManager) + serviceManager := reverseproxymanager.NewManager(store, am, permissionsManager, reverseproxymanager.NewGRPCProxyController(proxyServiceServer), domainManager) proxyServiceServer.SetServiceManager(serviceManager) am.SetServiceManager(serviceManager) diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index f7d07f3a0..f25a72181 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/go-version" "github.com/netbirdio/netbird/idp/dex" + rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" log "github.com/sirupsen/logrus" "github.com/netbirdio/netbird/management/server/types" @@ -51,6 +52,7 @@ type properties map[string]interface{} type DataSource interface { GetAllAccounts(ctx context.Context) []*types.Account GetStoreEngine() types.Engine + GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error) } // ConnManager peer connection manager that holds state for current active connections @@ -211,6 +213,16 @@ func (w *Worker) generateProperties(ctx context.Context) properties { localUsers int idpUsers int embeddedIdpTypes map[string]int + services int + servicesEnabled int + servicesTargets int + servicesStatusActive int + servicesStatusPending int + servicesStatusError int + servicesTargetType map[string]int + servicesAuthPassword int + servicesAuthPin int + servicesAuthOIDC int ) start := time.Now() metricsProperties := make(properties) @@ -220,10 +232,13 @@ func (w *Worker) generateProperties(ctx context.Context) properties { rulesDirection = make(map[string]int) activeUsersLastDay = make(map[string]struct{}) embeddedIdpTypes = make(map[string]int) + servicesTargetType = make(map[string]int) uptime = time.Since(w.startupTime).Seconds() connections := w.connManager.GetAllConnectedPeers() version = nbversion.NetbirdVersion() + customDomains, customDomainsValidated, _ := w.dataSource.GetCustomDomainsCounts(ctx) + for _, account := range w.dataSource.GetAllAccounts(ctx) { accounts++ @@ -335,6 +350,37 @@ func (w *Worker) generateProperties(ctx context.Context) properties { peerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion) } } + + for _, service := range account.Services { + services++ + if service.Enabled { + servicesEnabled++ + } + servicesTargets += len(service.Targets) + + switch rpservice.Status(service.Meta.Status) { + case rpservice.StatusActive: + servicesStatusActive++ + case rpservice.StatusPending: + servicesStatusPending++ + case rpservice.StatusError, rpservice.StatusCertificateFailed, rpservice.StatusTunnelNotCreated: + servicesStatusError++ + } + + for _, target := range service.Targets { + servicesTargetType[target.TargetType]++ + } + + if service.Auth.PasswordAuth != nil && service.Auth.PasswordAuth.Enabled { + servicesAuthPassword++ + } + if service.Auth.PinAuth != nil && service.Auth.PinAuth.Enabled { + servicesAuthPin++ + } + if service.Auth.BearerAuth != nil && service.Auth.BearerAuth.Enabled { + servicesAuthOIDC++ + } + } } minActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions) @@ -375,6 +421,22 @@ func (w *Worker) generateProperties(ctx context.Context) properties { metricsProperties["idp_users_count"] = idpUsers metricsProperties["embedded_idp_count"] = len(embeddedIdpTypes) + metricsProperties["services"] = services + metricsProperties["services_enabled"] = servicesEnabled + metricsProperties["services_targets"] = servicesTargets + metricsProperties["services_status_active"] = servicesStatusActive + metricsProperties["services_status_pending"] = servicesStatusPending + metricsProperties["services_status_error"] = servicesStatusError + metricsProperties["services_auth_password"] = servicesAuthPassword + metricsProperties["services_auth_pin"] = servicesAuthPin + metricsProperties["services_auth_oidc"] = servicesAuthOIDC + metricsProperties["custom_domains"] = customDomains + metricsProperties["custom_domains_validated"] = customDomainsValidated + + for targetType, count := range servicesTargetType { + metricsProperties["services_target_type_"+targetType] = count + } + for idpType, count := range embeddedIdpTypes { metricsProperties["embedded_idp_users_"+idpType] = count } diff --git a/management/server/metrics/selfhosted_test.go b/management/server/metrics/selfhosted_test.go index 504d228f7..7874e3a5f 100644 --- a/management/server/metrics/selfhosted_test.go +++ b/management/server/metrics/selfhosted_test.go @@ -6,6 +6,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/idp/dex" + rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -115,6 +116,31 @@ func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account { }, }, }, + Services: []*rpservice.Service{ + { + ID: "svc1", + Enabled: true, + Targets: []*rpservice.Target{ + {TargetType: "peer"}, + {TargetType: "host"}, + }, + Auth: rpservice.AuthConfig{ + PasswordAuth: &rpservice.PasswordAuthConfig{Enabled: true}, + }, + Meta: rpservice.ServiceMeta{Status: string(rpservice.StatusActive)}, + }, + { + ID: "svc2", + Enabled: false, + Targets: []*rpservice.Target{ + {TargetType: "domain"}, + }, + Auth: rpservice.AuthConfig{ + BearerAuth: &rpservice.BearerAuthConfig{Enabled: true}, + }, + Meta: rpservice.ServiceMeta{Status: string(rpservice.StatusPending)}, + }, + }, }, { Id: "2", @@ -215,6 +241,11 @@ func (mockDatasource) GetStoreEngine() types.Engine { return types.FileStoreEngine } +// GetCustomDomainsCounts returns test custom domain counts. +func (mockDatasource) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) { + return 3, 2, nil +} + // TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties func TestGenerateProperties(t *testing.T) { ds := mockDatasource{} @@ -347,6 +378,49 @@ func TestGenerateProperties(t *testing.T) { if properties["embedded_idp_count"] != 1 { t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"]) } + + if properties["services"] != 2 { + t.Errorf("expected 2 services, got %v", properties["services"]) + } + if properties["services_enabled"] != 1 { + t.Errorf("expected 1 services_enabled, got %v", properties["services_enabled"]) + } + if properties["services_targets"] != 3 { + t.Errorf("expected 3 services_targets, got %v", properties["services_targets"]) + } + if properties["services_status_active"] != 1 { + t.Errorf("expected 1 services_status_active, got %v", properties["services_status_active"]) + } + if properties["services_status_pending"] != 1 { + t.Errorf("expected 1 services_status_pending, got %v", properties["services_status_pending"]) + } + if properties["services_status_error"] != 0 { + t.Errorf("expected 0 services_status_error, got %v", properties["services_status_error"]) + } + if properties["services_target_type_peer"] != 1 { + t.Errorf("expected 1 services_target_type_peer, got %v", properties["services_target_type_peer"]) + } + if properties["services_target_type_host"] != 1 { + t.Errorf("expected 1 services_target_type_host, got %v", properties["services_target_type_host"]) + } + if properties["services_target_type_domain"] != 1 { + t.Errorf("expected 1 services_target_type_domain, got %v", properties["services_target_type_domain"]) + } + if properties["services_auth_password"] != 1 { + t.Errorf("expected 1 services_auth_password, got %v", properties["services_auth_password"]) + } + if properties["services_auth_oidc"] != 1 { + t.Errorf("expected 1 services_auth_oidc, got %v", properties["services_auth_oidc"]) + } + if properties["services_auth_pin"] != 0 { + t.Errorf("expected 0 services_auth_pin, got %v", properties["services_auth_pin"]) + } + if properties["custom_domains"] != int64(3) { + t.Errorf("expected 3 custom_domains, got %v", properties["custom_domains"]) + } + if properties["custom_domains_validated"] != int64(2) { + t.Errorf("expected 2 custom_domains_validated, got %v", properties["custom_domains_validated"]) + } } func TestExtractIdpType(t *testing.T) { diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 1ecda036d..afd2021ac 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -407,7 +407,7 @@ func (am *MockAccountManager) AddPeer( // GetGroupByName mock implementation of GetGroupByName from server.AccountManager interface func (am *MockAccountManager) GetGroupByName(ctx context.Context, accountID, groupName string) (*types.Group, error) { - if am.GetGroupFunc != nil { + if am.GetGroupByNameFunc != nil { return am.GetGroupByNameFunc(ctx, accountID, groupName) } return nil, status.Errorf(codes.Unimplemented, "method GetGroupByName is not implemented") diff --git a/management/server/networks/resources/manager_test.go b/management/server/networks/resources/manager_test.go index edf7ebefb..c6d8e7bcc 100644 --- a/management/server/networks/resources/manager_test.go +++ b/management/server/networks/resources/manager_test.go @@ -7,7 +7,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" + reverseproxy "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" "github.com/netbirdio/netbird/management/server/groups" "github.com/netbirdio/netbird/management/server/mock_server" "github.com/netbirdio/netbird/management/server/networks/resources/types" diff --git a/management/server/store/file_store.go b/management/server/store/file_store.go index 8db37ec30..81185b020 100644 --- a/management/server/store/file_store.go +++ b/management/server/store/file_store.go @@ -269,3 +269,8 @@ func (s *FileStore) GetStoreEngine() types.Engine { func (s *FileStore) SetFieldEncrypt(_ *crypt.FieldEncrypt) { // no-op: FileStore stores data in plaintext JSON; encryption is not supported } + +// GetCustomDomainsCounts is a no-op for FileStore as it doesn't support custom domains. +func (s *FileStore) GetCustomDomainsCounts(_ context.Context) (int64, int64, error) { + return 0, 0, nil +} diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 1190b595c..502619e07 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -1008,6 +1008,18 @@ func (s *SqlStore) GetAccountsCounter(ctx context.Context) (int64, error) { return count, nil } +// GetCustomDomainsCounts returns the total and validated custom domain counts. +func (s *SqlStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, error) { + var total, validated int64 + if err := s.db.WithContext(ctx).Model(&domain.Domain{}).Count(&total).Error; err != nil { + return 0, 0, err + } + if err := s.db.WithContext(ctx).Model(&domain.Domain{}).Where("validated = ?", true).Count(&validated).Error; err != nil { + return 0, 0, err + } + return total, validated, nil +} + func (s *SqlStore) GetAllAccounts(ctx context.Context) (all []*types.Account) { var accounts []types.Account result := s.db.Find(&accounts) @@ -2115,7 +2127,8 @@ func (s *SqlStore) getServices(ctx context.Context, accountID string) ([]*rpserv s.Meta.CreatedAt = createdAt.Time } if certIssuedAt.Valid { - s.Meta.CertificateIssuedAt = certIssuedAt.Time + t := certIssuedAt.Time + s.Meta.CertificateIssuedAt = &t } if status.Valid { s.Meta.Status = status.String diff --git a/management/server/store/store.go b/management/server/store/store.go index 60db74e2a..519e1503b 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -277,6 +277,8 @@ type Store interface { UpdateProxyHeartbeat(ctx context.Context, proxyID string) error GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error) CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error + + GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error) } const ( diff --git a/management/server/store/store_mock.go b/management/server/store/store_mock.go index 42bc94921..2a4c0d271 100644 --- a/management/server/store/store_mock.go +++ b/management/server/store/store_mock.go @@ -1887,6 +1887,22 @@ func (mr *MockStoreMockRecorder) GetServiceTargetByTargetID(ctx, lockStrength, a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceTargetByTargetID", reflect.TypeOf((*MockStore)(nil).GetServiceTargetByTargetID), ctx, lockStrength, accountID, targetID) } +// GetCustomDomainsCounts mocks base method. +func (m *MockStore) GetCustomDomainsCounts(ctx context.Context) (int64, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCustomDomainsCounts", ctx) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetCustomDomainsCounts indicates an expected call of GetCustomDomainsCounts. +func (mr *MockStoreMockRecorder) GetCustomDomainsCounts(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCustomDomainsCounts", reflect.TypeOf((*MockStore)(nil).GetCustomDomainsCounts), ctx) +} + // GetServices mocks base method. func (m *MockStore) GetServices(ctx context.Context, lockStrength LockingStrength) ([]*service.Service, error) { m.ctrl.T.Helper() diff --git a/management/server/types/settings.go b/management/server/types/settings.go index a94e01b78..e165968fc 100644 --- a/management/server/types/settings.go +++ b/management/server/types/settings.go @@ -47,6 +47,11 @@ type Settings struct { // NetworkRange is the custom network range for that account NetworkRange netip.Prefix `gorm:"serializer:json"` + // PeerExposeEnabled enables or disables peer-initiated service expose + PeerExposeEnabled bool + // PeerExposeGroups list of peer group IDs allowed to expose services + PeerExposeGroups []string `gorm:"serializer:json"` + // Extra is a dictionary of Account settings Extra *ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"` @@ -80,6 +85,8 @@ func (s *Settings) Copy() *Settings { PeerInactivityExpiration: s.PeerInactivityExpiration, RoutingPeerDNSResolutionEnabled: s.RoutingPeerDNSResolutionEnabled, + PeerExposeEnabled: s.PeerExposeEnabled, + PeerExposeGroups: slices.Clone(s.PeerExposeGroups), LazyConnectionEnabled: s.LazyConnectionEnabled, DNSDomain: s.DNSDomain, NetworkRange: s.NetworkRange, diff --git a/proxy/management_integration_test.go b/proxy/management_integration_test.go index 5ba7c90fa..47b2563df 100644 --- a/proxy/management_integration_test.go +++ b/proxy/management_integration_test.go @@ -300,6 +300,20 @@ func (m *storeBackedServiceManager) GetServiceIDByTargetID(ctx context.Context, return "", nil } +func (m *storeBackedServiceManager) CreateServiceFromPeer(_ context.Context, _, _ string, _ *reverseproxy.ExposeServiceRequest) (*reverseproxy.ExposeServiceResponse, error) { + return &reverseproxy.ExposeServiceResponse{}, nil +} + +func (m *storeBackedServiceManager) RenewServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *storeBackedServiceManager) StopServiceFromPeer(_ context.Context, _, _, _ string) error { + return nil +} + +func (m *storeBackedServiceManager) StartExposeReaper(_ context.Context) {} + func strPtr(s string) *string { return &s } diff --git a/shared/management/client/client.go b/shared/management/client/client.go index b92c636c5..ba525602e 100644 --- a/shared/management/client/client.go +++ b/shared/management/client/client.go @@ -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 } diff --git a/shared/management/client/grpc.go b/shared/management/client/grpc.go index d54c8f870..9505b3fdf 100644 --- a/shared/management/client/grpc.go +++ b/shared/management/client/grpc.go @@ -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 diff --git a/shared/management/client/mock.go b/shared/management/client/mock.go index ac96f7b36..57256d6d4 100644 --- a/shared/management/client/mock.go +++ b/shared/management/client/mock.go @@ -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) +} diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index b0ce1b5cc..2927d0319 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -326,6 +326,16 @@ components: type: string format: cidr example: 100.64.0.0/16 + peer_expose_enabled: + description: Enables or disables peer expose. If enabled, peers can expose local services through the reverse proxy using the CLI. + type: boolean + example: false + peer_expose_groups: + description: Limits which peer groups are allowed to expose services. If empty, all peers are allowed when peer expose is enabled. + type: array + items: + type: string + example: ch8i4ug6lnn4g9hqv7m0 extra: $ref: '#/components/schemas/AccountExtraSettings' lazy_connection_enabled: @@ -353,6 +363,8 @@ components: - peer_inactivity_expiration_enabled - peer_inactivity_expiration - regular_users_view_blocked + - peer_expose_enabled + - peer_expose_groups AccountExtraSettings: type: object properties: diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 7a7e75855..e53b876c2 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -512,6 +512,12 @@ type AccountSettings struct { // NetworkRange Allows to define a custom network range for the account in CIDR format NetworkRange *string `json:"network_range,omitempty"` + // PeerExposeEnabled Enables or disables peer expose. If enabled, peers can expose local services through the reverse proxy using the CLI. + PeerExposeEnabled bool `json:"peer_expose_enabled"` + + // PeerExposeGroups Limits which peer groups are allowed to expose services. If empty, all peers are allowed when peer expose is enabled. + PeerExposeGroups []string `json:"peer_expose_groups"` + // PeerInactivityExpiration Period of time of inactivity after which peer session expires (seconds). PeerInactivityExpiration int `json:"peer_inactivity_expiration"` diff --git a/shared/management/proto/management.pb.go b/shared/management/proto/management.pb.go index 44838fc16..97a2a4d18 100644 --- a/shared/management/proto/management.pb.go +++ b/shared/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v6.33.0 +// protoc v6.33.3 // source: management.proto package proto @@ -221,6 +221,58 @@ func (RuleAction) EnumDescriptor() ([]byte, []int) { return file_management_proto_rawDescGZIP(), []int{3} } +type ExposeProtocol int32 + +const ( + ExposeProtocol_EXPOSE_HTTP ExposeProtocol = 0 + ExposeProtocol_EXPOSE_HTTPS ExposeProtocol = 1 + ExposeProtocol_EXPOSE_TCP ExposeProtocol = 2 + ExposeProtocol_EXPOSE_UDP ExposeProtocol = 3 +) + +// Enum value maps for ExposeProtocol. +var ( + ExposeProtocol_name = map[int32]string{ + 0: "EXPOSE_HTTP", + 1: "EXPOSE_HTTPS", + 2: "EXPOSE_TCP", + 3: "EXPOSE_UDP", + } + ExposeProtocol_value = map[string]int32{ + "EXPOSE_HTTP": 0, + "EXPOSE_HTTPS": 1, + "EXPOSE_TCP": 2, + "EXPOSE_UDP": 3, + } +) + +func (x ExposeProtocol) Enum() *ExposeProtocol { + p := new(ExposeProtocol) + *p = x + return p +} + +func (x ExposeProtocol) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExposeProtocol) Descriptor() protoreflect.EnumDescriptor { + return file_management_proto_enumTypes[4].Descriptor() +} + +func (ExposeProtocol) Type() protoreflect.EnumType { + return &file_management_proto_enumTypes[4] +} + +func (x ExposeProtocol) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExposeProtocol.Descriptor instead. +func (ExposeProtocol) EnumDescriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{4} +} + type HostConfig_Protocol int32 const ( @@ -260,11 +312,11 @@ func (x HostConfig_Protocol) String() string { } func (HostConfig_Protocol) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[4].Descriptor() + return file_management_proto_enumTypes[5].Descriptor() } func (HostConfig_Protocol) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[4] + return &file_management_proto_enumTypes[5] } func (x HostConfig_Protocol) Number() protoreflect.EnumNumber { @@ -303,11 +355,11 @@ func (x DeviceAuthorizationFlowProvider) String() string { } func (DeviceAuthorizationFlowProvider) Descriptor() protoreflect.EnumDescriptor { - return file_management_proto_enumTypes[5].Descriptor() + return file_management_proto_enumTypes[6].Descriptor() } func (DeviceAuthorizationFlowProvider) Type() protoreflect.EnumType { - return &file_management_proto_enumTypes[5] + return &file_management_proto_enumTypes[6] } func (x DeviceAuthorizationFlowProvider) Number() protoreflect.EnumNumber { @@ -3983,6 +4035,334 @@ func (x *ForwardingRule) GetTranslatedPort() *PortInfo { return nil } +type ExposeServiceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Port uint32 `protobuf:"varint,1,opt,name=port,proto3" json:"port,omitempty"` + Protocol ExposeProtocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=management.ExposeProtocol" json:"protocol,omitempty"` + Pin string `protobuf:"bytes,3,opt,name=pin,proto3" json:"pin,omitempty"` + Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"` + UserGroups []string `protobuf:"bytes,5,rep,name=user_groups,json=userGroups,proto3" json:"user_groups,omitempty"` + Domain string `protobuf:"bytes,6,opt,name=domain,proto3" json:"domain,omitempty"` + NamePrefix string `protobuf:"bytes,7,opt,name=name_prefix,json=namePrefix,proto3" json:"name_prefix,omitempty"` +} + +func (x *ExposeServiceRequest) Reset() { + *x = ExposeServiceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExposeServiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposeServiceRequest) ProtoMessage() {} + +func (x *ExposeServiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[47] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExposeServiceRequest.ProtoReflect.Descriptor instead. +func (*ExposeServiceRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{47} +} + +func (x *ExposeServiceRequest) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *ExposeServiceRequest) GetProtocol() ExposeProtocol { + if x != nil { + return x.Protocol + } + return ExposeProtocol_EXPOSE_HTTP +} + +func (x *ExposeServiceRequest) GetPin() string { + if x != nil { + return x.Pin + } + return "" +} + +func (x *ExposeServiceRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *ExposeServiceRequest) GetUserGroups() []string { + if x != nil { + return x.UserGroups + } + return nil +} + +func (x *ExposeServiceRequest) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *ExposeServiceRequest) GetNamePrefix() string { + if x != nil { + return x.NamePrefix + } + return "" +} + +type ExposeServiceResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + ServiceUrl string `protobuf:"bytes,2,opt,name=service_url,json=serviceUrl,proto3" json:"service_url,omitempty"` + Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"` +} + +func (x *ExposeServiceResponse) Reset() { + *x = ExposeServiceResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExposeServiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposeServiceResponse) ProtoMessage() {} + +func (x *ExposeServiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[48] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExposeServiceResponse.ProtoReflect.Descriptor instead. +func (*ExposeServiceResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{48} +} + +func (x *ExposeServiceResponse) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +func (x *ExposeServiceResponse) GetServiceUrl() string { + if x != nil { + return x.ServiceUrl + } + return "" +} + +func (x *ExposeServiceResponse) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type RenewExposeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` +} + +func (x *RenewExposeRequest) Reset() { + *x = RenewExposeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RenewExposeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewExposeRequest) ProtoMessage() {} + +func (x *RenewExposeRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[49] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewExposeRequest.ProtoReflect.Descriptor instead. +func (*RenewExposeRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{49} +} + +func (x *RenewExposeRequest) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type RenewExposeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *RenewExposeResponse) Reset() { + *x = RenewExposeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RenewExposeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewExposeResponse) ProtoMessage() {} + +func (x *RenewExposeResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[50] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewExposeResponse.ProtoReflect.Descriptor instead. +func (*RenewExposeResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{50} +} + +type StopExposeRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` +} + +func (x *StopExposeRequest) Reset() { + *x = StopExposeRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StopExposeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopExposeRequest) ProtoMessage() {} + +func (x *StopExposeRequest) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[51] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StopExposeRequest.ProtoReflect.Descriptor instead. +func (*StopExposeRequest) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{51} +} + +func (x *StopExposeRequest) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type StopExposeResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StopExposeResponse) Reset() { + *x = StopExposeResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StopExposeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopExposeResponse) ProtoMessage() {} + +func (x *StopExposeResponse) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[52] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StopExposeResponse.ProtoReflect.Descriptor instead. +func (*StopExposeResponse) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{52} +} + type PortInfo_Range struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3995,7 +4375,7 @@ type PortInfo_Range struct { func (x *PortInfo_Range) Reset() { *x = PortInfo_Range{} if protoimpl.UnsafeEnabled { - mi := &file_management_proto_msgTypes[48] + mi := &file_management_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4008,7 +4388,7 @@ func (x *PortInfo_Range) String() string { func (*PortInfo_Range) ProtoMessage() {} func (x *PortInfo_Range) ProtoReflect() protoreflect.Message { - mi := &file_management_proto_msgTypes[48] + mi := &file_management_proto_msgTypes[54] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4616,62 +4996,113 @@ var file_management_proto_rawDesc = []byte{ 0x64, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x50, 0x6f, 0x72, - 0x74, 0x2a, 0x3a, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, - 0x0a, 0x0e, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, - 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, - 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, - 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, - 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, - 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, - 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, - 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, - 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, - 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, - 0x01, 0x32, 0x96, 0x05, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, - 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x74, 0x22, 0xea, 0x01, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x36, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, + 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, + 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x67, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1f, 0x0a, + 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0x73, + 0x0a, 0x15, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x22, 0x2c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x11, 0x53, 0x74, 0x6f, 0x70, + 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, + 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, + 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x3a, 0x0a, 0x09, 0x4a, + 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x75, 0x6e, 0x6b, 0x6e, + 0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, + 0x73, 0x75, 0x63, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x66, + 0x61, 0x69, 0x6c, 0x65, 0x64, 0x10, 0x02, 0x2a, 0x4c, 0x0a, 0x0c, 0x52, 0x75, 0x6c, 0x65, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, + 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, + 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, + 0x54, 0x4f, 0x4d, 0x10, 0x05, 0x2a, 0x20, 0x0a, 0x0d, 0x52, 0x75, 0x6c, 0x65, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, + 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x2a, 0x22, 0x0a, 0x0a, 0x52, 0x75, 0x6c, 0x65, 0x41, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, + 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x2a, 0x53, 0x0a, 0x0e, 0x45, + 0x78, 0x70, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0f, 0x0a, + 0x0b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x10, 0x00, 0x12, 0x10, + 0x0a, 0x0c, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x53, 0x10, 0x01, + 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x54, 0x43, 0x50, 0x10, 0x02, + 0x12, 0x0e, 0x0a, 0x0a, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x55, 0x44, 0x50, 0x10, 0x03, + 0x32, 0xfd, 0x06, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, + 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, + 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, + 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, - 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, - 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, - 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x47, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x08, 0x53, 0x79, 0x6e, 0x63, 0x4d, 0x65, 0x74, - 0x61, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, - 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x06, 0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x12, 0x1c, - 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, - 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x11, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, - 0x00, 0x12, 0x47, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4c, 0x0a, 0x0c, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x6e, 0x65, 0x77, + 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x4a, 0x0a, 0x0a, 0x53, 0x74, 0x6f, 0x70, 0x45, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, + 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, + 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -4686,152 +5117,166 @@ func file_management_proto_rawDescGZIP() []byte { return file_management_proto_rawDescData } -var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 49) +var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 7) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 55) var file_management_proto_goTypes = []interface{}{ (JobStatus)(0), // 0: management.JobStatus (RuleProtocol)(0), // 1: management.RuleProtocol (RuleDirection)(0), // 2: management.RuleDirection (RuleAction)(0), // 3: management.RuleAction - (HostConfig_Protocol)(0), // 4: management.HostConfig.Protocol - (DeviceAuthorizationFlowProvider)(0), // 5: management.DeviceAuthorizationFlow.provider - (*EncryptedMessage)(nil), // 6: management.EncryptedMessage - (*JobRequest)(nil), // 7: management.JobRequest - (*JobResponse)(nil), // 8: management.JobResponse - (*BundleParameters)(nil), // 9: management.BundleParameters - (*BundleResult)(nil), // 10: management.BundleResult - (*SyncRequest)(nil), // 11: management.SyncRequest - (*SyncResponse)(nil), // 12: management.SyncResponse - (*SyncMetaRequest)(nil), // 13: management.SyncMetaRequest - (*LoginRequest)(nil), // 14: management.LoginRequest - (*PeerKeys)(nil), // 15: management.PeerKeys - (*Environment)(nil), // 16: management.Environment - (*File)(nil), // 17: management.File - (*Flags)(nil), // 18: management.Flags - (*PeerSystemMeta)(nil), // 19: management.PeerSystemMeta - (*LoginResponse)(nil), // 20: management.LoginResponse - (*ServerKeyResponse)(nil), // 21: management.ServerKeyResponse - (*Empty)(nil), // 22: management.Empty - (*NetbirdConfig)(nil), // 23: management.NetbirdConfig - (*HostConfig)(nil), // 24: management.HostConfig - (*RelayConfig)(nil), // 25: management.RelayConfig - (*FlowConfig)(nil), // 26: management.FlowConfig - (*JWTConfig)(nil), // 27: management.JWTConfig - (*ProtectedHostConfig)(nil), // 28: management.ProtectedHostConfig - (*PeerConfig)(nil), // 29: management.PeerConfig - (*AutoUpdateSettings)(nil), // 30: management.AutoUpdateSettings - (*NetworkMap)(nil), // 31: management.NetworkMap - (*SSHAuth)(nil), // 32: management.SSHAuth - (*MachineUserIndexes)(nil), // 33: management.MachineUserIndexes - (*RemotePeerConfig)(nil), // 34: management.RemotePeerConfig - (*SSHConfig)(nil), // 35: management.SSHConfig - (*DeviceAuthorizationFlowRequest)(nil), // 36: management.DeviceAuthorizationFlowRequest - (*DeviceAuthorizationFlow)(nil), // 37: management.DeviceAuthorizationFlow - (*PKCEAuthorizationFlowRequest)(nil), // 38: management.PKCEAuthorizationFlowRequest - (*PKCEAuthorizationFlow)(nil), // 39: management.PKCEAuthorizationFlow - (*ProviderConfig)(nil), // 40: management.ProviderConfig - (*Route)(nil), // 41: management.Route - (*DNSConfig)(nil), // 42: management.DNSConfig - (*CustomZone)(nil), // 43: management.CustomZone - (*SimpleRecord)(nil), // 44: management.SimpleRecord - (*NameServerGroup)(nil), // 45: management.NameServerGroup - (*NameServer)(nil), // 46: management.NameServer - (*FirewallRule)(nil), // 47: management.FirewallRule - (*NetworkAddress)(nil), // 48: management.NetworkAddress - (*Checks)(nil), // 49: management.Checks - (*PortInfo)(nil), // 50: management.PortInfo - (*RouteFirewallRule)(nil), // 51: management.RouteFirewallRule - (*ForwardingRule)(nil), // 52: management.ForwardingRule - nil, // 53: management.SSHAuth.MachineUsersEntry - (*PortInfo_Range)(nil), // 54: management.PortInfo.Range - (*timestamppb.Timestamp)(nil), // 55: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 56: google.protobuf.Duration + (ExposeProtocol)(0), // 4: management.ExposeProtocol + (HostConfig_Protocol)(0), // 5: management.HostConfig.Protocol + (DeviceAuthorizationFlowProvider)(0), // 6: management.DeviceAuthorizationFlow.provider + (*EncryptedMessage)(nil), // 7: management.EncryptedMessage + (*JobRequest)(nil), // 8: management.JobRequest + (*JobResponse)(nil), // 9: management.JobResponse + (*BundleParameters)(nil), // 10: management.BundleParameters + (*BundleResult)(nil), // 11: management.BundleResult + (*SyncRequest)(nil), // 12: management.SyncRequest + (*SyncResponse)(nil), // 13: management.SyncResponse + (*SyncMetaRequest)(nil), // 14: management.SyncMetaRequest + (*LoginRequest)(nil), // 15: management.LoginRequest + (*PeerKeys)(nil), // 16: management.PeerKeys + (*Environment)(nil), // 17: management.Environment + (*File)(nil), // 18: management.File + (*Flags)(nil), // 19: management.Flags + (*PeerSystemMeta)(nil), // 20: management.PeerSystemMeta + (*LoginResponse)(nil), // 21: management.LoginResponse + (*ServerKeyResponse)(nil), // 22: management.ServerKeyResponse + (*Empty)(nil), // 23: management.Empty + (*NetbirdConfig)(nil), // 24: management.NetbirdConfig + (*HostConfig)(nil), // 25: management.HostConfig + (*RelayConfig)(nil), // 26: management.RelayConfig + (*FlowConfig)(nil), // 27: management.FlowConfig + (*JWTConfig)(nil), // 28: management.JWTConfig + (*ProtectedHostConfig)(nil), // 29: management.ProtectedHostConfig + (*PeerConfig)(nil), // 30: management.PeerConfig + (*AutoUpdateSettings)(nil), // 31: management.AutoUpdateSettings + (*NetworkMap)(nil), // 32: management.NetworkMap + (*SSHAuth)(nil), // 33: management.SSHAuth + (*MachineUserIndexes)(nil), // 34: management.MachineUserIndexes + (*RemotePeerConfig)(nil), // 35: management.RemotePeerConfig + (*SSHConfig)(nil), // 36: management.SSHConfig + (*DeviceAuthorizationFlowRequest)(nil), // 37: management.DeviceAuthorizationFlowRequest + (*DeviceAuthorizationFlow)(nil), // 38: management.DeviceAuthorizationFlow + (*PKCEAuthorizationFlowRequest)(nil), // 39: management.PKCEAuthorizationFlowRequest + (*PKCEAuthorizationFlow)(nil), // 40: management.PKCEAuthorizationFlow + (*ProviderConfig)(nil), // 41: management.ProviderConfig + (*Route)(nil), // 42: management.Route + (*DNSConfig)(nil), // 43: management.DNSConfig + (*CustomZone)(nil), // 44: management.CustomZone + (*SimpleRecord)(nil), // 45: management.SimpleRecord + (*NameServerGroup)(nil), // 46: management.NameServerGroup + (*NameServer)(nil), // 47: management.NameServer + (*FirewallRule)(nil), // 48: management.FirewallRule + (*NetworkAddress)(nil), // 49: management.NetworkAddress + (*Checks)(nil), // 50: management.Checks + (*PortInfo)(nil), // 51: management.PortInfo + (*RouteFirewallRule)(nil), // 52: management.RouteFirewallRule + (*ForwardingRule)(nil), // 53: management.ForwardingRule + (*ExposeServiceRequest)(nil), // 54: management.ExposeServiceRequest + (*ExposeServiceResponse)(nil), // 55: management.ExposeServiceResponse + (*RenewExposeRequest)(nil), // 56: management.RenewExposeRequest + (*RenewExposeResponse)(nil), // 57: management.RenewExposeResponse + (*StopExposeRequest)(nil), // 58: management.StopExposeRequest + (*StopExposeResponse)(nil), // 59: management.StopExposeResponse + nil, // 60: management.SSHAuth.MachineUsersEntry + (*PortInfo_Range)(nil), // 61: management.PortInfo.Range + (*timestamppb.Timestamp)(nil), // 62: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 63: google.protobuf.Duration } var file_management_proto_depIdxs = []int32{ - 9, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters + 10, // 0: management.JobRequest.bundle:type_name -> management.BundleParameters 0, // 1: management.JobResponse.status:type_name -> management.JobStatus - 10, // 2: management.JobResponse.bundle:type_name -> management.BundleResult - 19, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta - 23, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig - 29, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig - 34, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig - 31, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap - 49, // 8: management.SyncResponse.Checks:type_name -> management.Checks - 19, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta - 19, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta - 15, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys - 48, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress - 16, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment - 17, // 14: management.PeerSystemMeta.files:type_name -> management.File - 18, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags - 23, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig - 29, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 49, // 18: management.LoginResponse.Checks:type_name -> management.Checks - 55, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp - 24, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig - 28, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig - 24, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig - 25, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig - 26, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig - 4, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol - 56, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration - 24, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig - 35, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig - 30, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings - 29, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig - 34, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 41, // 32: management.NetworkMap.Routes:type_name -> management.Route - 42, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig - 34, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig - 47, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule - 51, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule - 52, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule - 32, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth - 53, // 39: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry - 35, // 40: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 27, // 41: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig - 5, // 42: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 40, // 43: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 40, // 44: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 45, // 45: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup - 43, // 46: management.DNSConfig.CustomZones:type_name -> management.CustomZone - 44, // 47: management.CustomZone.Records:type_name -> management.SimpleRecord - 46, // 48: management.NameServerGroup.NameServers:type_name -> management.NameServer + 11, // 2: management.JobResponse.bundle:type_name -> management.BundleResult + 20, // 3: management.SyncRequest.meta:type_name -> management.PeerSystemMeta + 24, // 4: management.SyncResponse.netbirdConfig:type_name -> management.NetbirdConfig + 30, // 5: management.SyncResponse.peerConfig:type_name -> management.PeerConfig + 35, // 6: management.SyncResponse.remotePeers:type_name -> management.RemotePeerConfig + 32, // 7: management.SyncResponse.NetworkMap:type_name -> management.NetworkMap + 50, // 8: management.SyncResponse.Checks:type_name -> management.Checks + 20, // 9: management.SyncMetaRequest.meta:type_name -> management.PeerSystemMeta + 20, // 10: management.LoginRequest.meta:type_name -> management.PeerSystemMeta + 16, // 11: management.LoginRequest.peerKeys:type_name -> management.PeerKeys + 49, // 12: management.PeerSystemMeta.networkAddresses:type_name -> management.NetworkAddress + 17, // 13: management.PeerSystemMeta.environment:type_name -> management.Environment + 18, // 14: management.PeerSystemMeta.files:type_name -> management.File + 19, // 15: management.PeerSystemMeta.flags:type_name -> management.Flags + 24, // 16: management.LoginResponse.netbirdConfig:type_name -> management.NetbirdConfig + 30, // 17: management.LoginResponse.peerConfig:type_name -> management.PeerConfig + 50, // 18: management.LoginResponse.Checks:type_name -> management.Checks + 62, // 19: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 25, // 20: management.NetbirdConfig.stuns:type_name -> management.HostConfig + 29, // 21: management.NetbirdConfig.turns:type_name -> management.ProtectedHostConfig + 25, // 22: management.NetbirdConfig.signal:type_name -> management.HostConfig + 26, // 23: management.NetbirdConfig.relay:type_name -> management.RelayConfig + 27, // 24: management.NetbirdConfig.flow:type_name -> management.FlowConfig + 5, // 25: management.HostConfig.protocol:type_name -> management.HostConfig.Protocol + 63, // 26: management.FlowConfig.interval:type_name -> google.protobuf.Duration + 25, // 27: management.ProtectedHostConfig.hostConfig:type_name -> management.HostConfig + 36, // 28: management.PeerConfig.sshConfig:type_name -> management.SSHConfig + 31, // 29: management.PeerConfig.autoUpdate:type_name -> management.AutoUpdateSettings + 30, // 30: management.NetworkMap.peerConfig:type_name -> management.PeerConfig + 35, // 31: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig + 42, // 32: management.NetworkMap.Routes:type_name -> management.Route + 43, // 33: management.NetworkMap.DNSConfig:type_name -> management.DNSConfig + 35, // 34: management.NetworkMap.offlinePeers:type_name -> management.RemotePeerConfig + 48, // 35: management.NetworkMap.FirewallRules:type_name -> management.FirewallRule + 52, // 36: management.NetworkMap.routesFirewallRules:type_name -> management.RouteFirewallRule + 53, // 37: management.NetworkMap.forwardingRules:type_name -> management.ForwardingRule + 33, // 38: management.NetworkMap.sshAuth:type_name -> management.SSHAuth + 60, // 39: management.SSHAuth.machine_users:type_name -> management.SSHAuth.MachineUsersEntry + 36, // 40: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 28, // 41: management.SSHConfig.jwtConfig:type_name -> management.JWTConfig + 6, // 42: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 41, // 43: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 41, // 44: management.PKCEAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 46, // 45: management.DNSConfig.NameServerGroups:type_name -> management.NameServerGroup + 44, // 46: management.DNSConfig.CustomZones:type_name -> management.CustomZone + 45, // 47: management.CustomZone.Records:type_name -> management.SimpleRecord + 47, // 48: management.NameServerGroup.NameServers:type_name -> management.NameServer 2, // 49: management.FirewallRule.Direction:type_name -> management.RuleDirection 3, // 50: management.FirewallRule.Action:type_name -> management.RuleAction 1, // 51: management.FirewallRule.Protocol:type_name -> management.RuleProtocol - 50, // 52: management.FirewallRule.PortInfo:type_name -> management.PortInfo - 54, // 53: management.PortInfo.range:type_name -> management.PortInfo.Range + 51, // 52: management.FirewallRule.PortInfo:type_name -> management.PortInfo + 61, // 53: management.PortInfo.range:type_name -> management.PortInfo.Range 3, // 54: management.RouteFirewallRule.action:type_name -> management.RuleAction 1, // 55: management.RouteFirewallRule.protocol:type_name -> management.RuleProtocol - 50, // 56: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo + 51, // 56: management.RouteFirewallRule.portInfo:type_name -> management.PortInfo 1, // 57: management.ForwardingRule.protocol:type_name -> management.RuleProtocol - 50, // 58: management.ForwardingRule.destinationPort:type_name -> management.PortInfo - 50, // 59: management.ForwardingRule.translatedPort:type_name -> management.PortInfo - 33, // 60: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes - 6, // 61: management.ManagementService.Login:input_type -> management.EncryptedMessage - 6, // 62: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 22, // 63: management.ManagementService.GetServerKey:input_type -> management.Empty - 22, // 64: management.ManagementService.isHealthy:input_type -> management.Empty - 6, // 65: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 6, // 66: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage - 6, // 67: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage - 6, // 68: management.ManagementService.Logout:input_type -> management.EncryptedMessage - 6, // 69: management.ManagementService.Job:input_type -> management.EncryptedMessage - 6, // 70: management.ManagementService.Login:output_type -> management.EncryptedMessage - 6, // 71: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 21, // 72: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 22, // 73: management.ManagementService.isHealthy:output_type -> management.Empty - 6, // 74: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 6, // 75: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage - 22, // 76: management.ManagementService.SyncMeta:output_type -> management.Empty - 22, // 77: management.ManagementService.Logout:output_type -> management.Empty - 6, // 78: management.ManagementService.Job:output_type -> management.EncryptedMessage - 70, // [70:79] is the sub-list for method output_type - 61, // [61:70] is the sub-list for method input_type - 61, // [61:61] is the sub-list for extension type_name - 61, // [61:61] is the sub-list for extension extendee - 0, // [0:61] is the sub-list for field type_name + 51, // 58: management.ForwardingRule.destinationPort:type_name -> management.PortInfo + 51, // 59: management.ForwardingRule.translatedPort:type_name -> management.PortInfo + 4, // 60: management.ExposeServiceRequest.protocol:type_name -> management.ExposeProtocol + 34, // 61: management.SSHAuth.MachineUsersEntry.value:type_name -> management.MachineUserIndexes + 7, // 62: management.ManagementService.Login:input_type -> management.EncryptedMessage + 7, // 63: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 23, // 64: management.ManagementService.GetServerKey:input_type -> management.Empty + 23, // 65: management.ManagementService.isHealthy:input_type -> management.Empty + 7, // 66: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 7, // 67: management.ManagementService.GetPKCEAuthorizationFlow:input_type -> management.EncryptedMessage + 7, // 68: management.ManagementService.SyncMeta:input_type -> management.EncryptedMessage + 7, // 69: management.ManagementService.Logout:input_type -> management.EncryptedMessage + 7, // 70: management.ManagementService.Job:input_type -> management.EncryptedMessage + 7, // 71: management.ManagementService.CreateExpose:input_type -> management.EncryptedMessage + 7, // 72: management.ManagementService.RenewExpose:input_type -> management.EncryptedMessage + 7, // 73: management.ManagementService.StopExpose:input_type -> management.EncryptedMessage + 7, // 74: management.ManagementService.Login:output_type -> management.EncryptedMessage + 7, // 75: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 22, // 76: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 23, // 77: management.ManagementService.isHealthy:output_type -> management.Empty + 7, // 78: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 7, // 79: management.ManagementService.GetPKCEAuthorizationFlow:output_type -> management.EncryptedMessage + 23, // 80: management.ManagementService.SyncMeta:output_type -> management.Empty + 23, // 81: management.ManagementService.Logout:output_type -> management.Empty + 7, // 82: management.ManagementService.Job:output_type -> management.EncryptedMessage + 7, // 83: management.ManagementService.CreateExpose:output_type -> management.EncryptedMessage + 7, // 84: management.ManagementService.RenewExpose:output_type -> management.EncryptedMessage + 7, // 85: management.ManagementService.StopExpose:output_type -> management.EncryptedMessage + 74, // [74:86] is the sub-list for method output_type + 62, // [62:74] is the sub-list for method input_type + 62, // [62:62] is the sub-list for extension type_name + 62, // [62:62] is the sub-list for extension extendee + 0, // [0:62] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -5404,7 +5849,79 @@ func file_management_proto_init() { return nil } } + file_management_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExposeServiceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } file_management_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExposeServiceResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RenewExposeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RenewExposeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopExposeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[52].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopExposeResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_management_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PortInfo_Range); i { case 0: return &v.state @@ -5432,8 +5949,8 @@ func file_management_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, - NumEnums: 6, - NumMessages: 49, + NumEnums: 7, + NumMessages: 55, NumExtensions: 0, NumServices: 1, }, diff --git a/shared/management/proto/management.proto b/shared/management/proto/management.proto index d97d66819..3667ae27f 100644 --- a/shared/management/proto/management.proto +++ b/shared/management/proto/management.proto @@ -51,6 +51,15 @@ service ManagementService { // Executes a job on a target peer (e.g., debug bundle) rpc Job(stream EncryptedMessage) returns (stream EncryptedMessage) {} + + // CreateExpose creates a temporary reverse proxy service for a peer + rpc CreateExpose(EncryptedMessage) returns (EncryptedMessage) {} + + // RenewExpose extends the TTL of an active expose session + rpc RenewExpose(EncryptedMessage) returns (EncryptedMessage) {} + + // StopExpose terminates an active expose session + rpc StopExpose(EncryptedMessage) returns (EncryptedMessage) {} } message EncryptedMessage { @@ -637,3 +646,38 @@ message ForwardingRule { // Translated port information, where the traffic should be forwarded to PortInfo translatedPort = 4; } + +enum ExposeProtocol { + EXPOSE_HTTP = 0; + EXPOSE_HTTPS = 1; + EXPOSE_TCP = 2; + EXPOSE_UDP = 3; +} + +message ExposeServiceRequest { + uint32 port = 1; + ExposeProtocol protocol = 2; + string pin = 3; + string password = 4; + repeated string user_groups = 5; + string domain = 6; + string name_prefix = 7; +} + +message ExposeServiceResponse { + string service_name = 1; + string service_url = 2; + string domain = 3; +} + +message RenewExposeRequest { + string domain = 1; +} + +message RenewExposeResponse {} + +message StopExposeRequest { + string domain = 1; +} + +message StopExposeResponse {} diff --git a/shared/management/proto/management_grpc.pb.go b/shared/management/proto/management_grpc.pb.go index b78e21aaa..39a342041 100644 --- a/shared/management/proto/management_grpc.pb.go +++ b/shared/management/proto/management_grpc.pb.go @@ -52,6 +52,12 @@ type ManagementServiceClient interface { Logout(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*Empty, error) // Executes a job on a target peer (e.g., debug bundle) Job(ctx context.Context, opts ...grpc.CallOption) (ManagementService_JobClient, error) + // CreateExpose creates a temporary reverse proxy service for a peer + CreateExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) + // RenewExpose extends the TTL of an active expose session + RenewExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) + // StopExpose terminates an active expose session + StopExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) } type managementServiceClient struct { @@ -188,6 +194,33 @@ func (x *managementServiceJobClient) Recv() (*EncryptedMessage, error) { return m, nil } +func (c *managementServiceClient) CreateExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + out := new(EncryptedMessage) + err := c.cc.Invoke(ctx, "/management.ManagementService/CreateExpose", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *managementServiceClient) RenewExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + out := new(EncryptedMessage) + err := c.cc.Invoke(ctx, "/management.ManagementService/RenewExpose", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *managementServiceClient) StopExpose(ctx context.Context, in *EncryptedMessage, opts ...grpc.CallOption) (*EncryptedMessage, error) { + out := new(EncryptedMessage) + err := c.cc.Invoke(ctx, "/management.ManagementService/StopExpose", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ManagementServiceServer is the server API for ManagementService service. // All implementations must embed UnimplementedManagementServiceServer // for forward compatibility @@ -226,6 +259,12 @@ type ManagementServiceServer interface { Logout(context.Context, *EncryptedMessage) (*Empty, error) // Executes a job on a target peer (e.g., debug bundle) Job(ManagementService_JobServer) error + // CreateExpose creates a temporary reverse proxy service for a peer + CreateExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) + // RenewExpose extends the TTL of an active expose session + RenewExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) + // StopExpose terminates an active expose session + StopExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) mustEmbedUnimplementedManagementServiceServer() } @@ -260,6 +299,15 @@ func (UnimplementedManagementServiceServer) Logout(context.Context, *EncryptedMe func (UnimplementedManagementServiceServer) Job(ManagementService_JobServer) error { return status.Errorf(codes.Unimplemented, "method Job not implemented") } +func (UnimplementedManagementServiceServer) CreateExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateExpose not implemented") +} +func (UnimplementedManagementServiceServer) RenewExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method RenewExpose not implemented") +} +func (UnimplementedManagementServiceServer) StopExpose(context.Context, *EncryptedMessage) (*EncryptedMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method StopExpose not implemented") +} func (UnimplementedManagementServiceServer) mustEmbedUnimplementedManagementServiceServer() {} // UnsafeManagementServiceServer may be embedded to opt out of forward compatibility for this service. @@ -446,6 +494,60 @@ func (x *managementServiceJobServer) Recv() (*EncryptedMessage, error) { return m, nil } +func _ManagementService_CreateExpose_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).CreateExpose(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/CreateExpose", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).CreateExpose(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _ManagementService_RenewExpose_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).RenewExpose(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/RenewExpose", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).RenewExpose(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _ManagementService_StopExpose_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EncryptedMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ManagementServiceServer).StopExpose(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/management.ManagementService/StopExpose", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ManagementServiceServer).StopExpose(ctx, req.(*EncryptedMessage)) + } + return interceptor(ctx, in, info, handler) +} + // ManagementService_ServiceDesc is the grpc.ServiceDesc for ManagementService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -481,6 +583,18 @@ var ManagementService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Logout", Handler: _ManagementService_Logout_Handler, }, + { + MethodName: "CreateExpose", + Handler: _ManagementService_CreateExpose_Handler, + }, + { + MethodName: "RenewExpose", + Handler: _ManagementService_RenewExpose_Handler, + }, + { + MethodName: "StopExpose", + Handler: _ManagementService_StopExpose_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/shared/management/proto/proxy_service.pb.go b/shared/management/proto/proxy_service.pb.go index 13fcb159e..c89157eb5 100644 --- a/shared/management/proto/proxy_service.pb.go +++ b/shared/management/proto/proxy_service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v6.33.0 +// protoc v6.33.3 // source: proxy_service.proto package proto