From 5a3d9e401fc65703731cc752af06ce1656935932 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Fri, 8 Mar 2024 20:28:13 +0300 Subject: [PATCH] Send terminal notification on peer session expiry (#1660) Send notification through terminal on user session expiration in Linux and macOS, unless UI application is installed to handle it instead. --- client/internal/peer/status.go | 21 +++++++++ client/internal/session.go | 82 ++++++++++++++++++++++++++++++++++ client/server/server.go | 47 +++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 client/internal/session.go diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 9a1b7ab83..87338f646 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -5,6 +5,9 @@ import ( "sync" "time" + "google.golang.org/grpc/codes" + gstatus "google.golang.org/grpc/status" + "github.com/netbirdio/netbird/client/internal/relay" "github.com/netbirdio/netbird/iface" ) @@ -376,6 +379,24 @@ func (d *Status) GetManagementState() ManagementState { } } +// IsLoginRequired determines if a peer's login has expired. +func (d *Status) IsLoginRequired() bool { + d.mux.Lock() + defer d.mux.Unlock() + + // if peer is connected to the management then login is not expired + if d.managementState { + return false + } + + s, ok := gstatus.FromError(d.managementError) + if ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) { + return true + + } + return false +} + func (d *Status) GetSignalState() SignalState { return SignalState{ d.signalAddress, diff --git a/client/internal/session.go b/client/internal/session.go new file mode 100644 index 000000000..49dc95f6a --- /dev/null +++ b/client/internal/session.go @@ -0,0 +1,82 @@ +package internal + +import ( + "context" + "os/exec" + "strings" + "sync" + "time" + + "github.com/netbirdio/netbird/client/internal/peer" +) + +type SessionWatcher struct { + ctx context.Context + mutex sync.Mutex + + peerStatusRecorder *peer.Status + watchTicker *time.Ticker + + sendNotification bool + onExpireListener func() +} + +// NewSessionWatcher creates a new instance of SessionWatcher. +func NewSessionWatcher(ctx context.Context, peerStatusRecorder *peer.Status) *SessionWatcher { + s := &SessionWatcher{ + ctx: ctx, + peerStatusRecorder: peerStatusRecorder, + watchTicker: time.NewTicker(2 * time.Second), + } + go s.startWatcher() + return s +} + +// SetOnExpireListener sets the callback func to be called when the session expires. +func (s *SessionWatcher) SetOnExpireListener(onExpire func()) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.onExpireListener = onExpire +} + +// startWatcher continuously checks if the session requires login and +// calls the onExpireListener if login is required. +func (s *SessionWatcher) startWatcher() { + for { + select { + case <-s.ctx.Done(): + s.watchTicker.Stop() + return + case <-s.watchTicker.C: + managementState := s.peerStatusRecorder.GetManagementState() + if managementState.Connected { + s.sendNotification = true + } + + isLoginRequired := s.peerStatusRecorder.IsLoginRequired() + if isLoginRequired && s.sendNotification && s.onExpireListener != nil { + s.mutex.Lock() + s.onExpireListener() + s.sendNotification = false + s.mutex.Unlock() + } + } + } +} + +// CheckUIApp checks whether UI application is running. +func CheckUIApp() bool { + cmd := exec.Command("ps", "-ef") + output, err := cmd.Output() + if err != nil { + return false + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "netbird-ui") && !strings.Contains(line, "grep") { + return true + } + } + return false +} diff --git a/client/server/server.go b/client/server/server.go index 71084adff..fc1e4cc26 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -3,6 +3,8 @@ package server import ( "context" "fmt" + "os/exec" + "runtime" "sync" "time" @@ -39,6 +41,7 @@ type Server struct { proto.UnimplementedDaemonServiceServer statusRecorder *peer.Status + sessionWatcher *internal.SessionWatcher mgmProbe *internal.Probe signalProbe *internal.Probe @@ -116,6 +119,11 @@ func (s *Server) Start() error { s.statusRecorder.UpdateManagementAddress(config.ManagementURL.String()) s.statusRecorder.UpdateRosenpass(config.RosenpassEnabled, config.RosenpassPermissive) + if s.sessionWatcher == nil { + s.sessionWatcher = internal.NewSessionWatcher(s.rootCtx, s.statusRecorder) + s.sessionWatcher.SetOnExpireListener(s.onSessionExpire) + } + if !config.DisableAutoConnect { go func() { if err := internal.RunClientWithProbes(ctx, config, s.statusRecorder, s.mgmProbe, s.signalProbe, s.relayProbe, s.wgProbe); err != nil { @@ -542,6 +550,17 @@ func (s *Server) GetConfig(_ context.Context, _ *proto.GetConfigRequest) (*proto }, nil } +func (s *Server) onSessionExpire() { + if runtime.GOOS != "windows" { + isUIActive := internal.CheckUIApp() + if !isUIActive { + if err := sendTerminalNotification(); err != nil { + log.Errorf("send session expire terminal notification: %v", err) + } + } + } +} + func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { pbFullStatus := proto.FullStatus{ ManagementState: &proto.ManagementState{}, @@ -604,3 +623,31 @@ func toProtoFullStatus(fullStatus peer.FullStatus) *proto.FullStatus { return &pbFullStatus } + +// sendTerminalNotification sends a terminal notification message +// to inform the user that the NetBird connection session has expired. +func sendTerminalNotification() error { + message := "NetBird connection session expired\n\nPlease re-authenticate to connect to the network." + echoCmd := exec.Command("echo", message) + wallCmd := exec.Command("sudo", "wall") + + echoCmdStdout, err := echoCmd.StdoutPipe() + if err != nil { + return err + } + wallCmd.Stdin = echoCmdStdout + + if err := echoCmd.Start(); err != nil { + return err + } + + if err := wallCmd.Start(); err != nil { + return err + } + + if err := echoCmd.Wait(); err != nil { + return err + } + + return wallCmd.Wait() +}