diff --git a/client/cmd/service.go b/client/cmd/service.go index 7410d60ea..7d477ec79 100644 --- a/client/cmd/service.go +++ b/client/cmd/service.go @@ -15,6 +15,7 @@ import ( "google.golang.org/grpc" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/ipcauth" "github.com/netbirdio/netbird/client/server" ) @@ -45,6 +46,7 @@ type program struct { jsonServMu sync.Mutex serverInstance *server.Server serverInstanceMu sync.Mutex + policyGate *ipcauth.PolicyGate } func init() { diff --git a/client/cmd/service_controller.go b/client/cmd/service_controller.go index e9a0e055f..1feed57bb 100644 --- a/client/cmd/service_controller.go +++ b/client/cmd/service_controller.go @@ -80,8 +80,14 @@ func (p *program) Start(svc service.Service) error { return fmt.Errorf("parse daemon address: %w", err) } + p.policyGate = ipcauth.NewPolicyGate() + // in any case, even if configuration does not exists we run daemon to serve CLI gRPC API. - p.serv = grpc.NewServer(daemonServerOptions(network)...) + opts := append(daemonServerOptions(network), + grpc.ChainUnaryInterceptor(p.policyGate.UnaryPolicyInterceptor()), + grpc.ChainStreamInterceptor(p.policyGate.StreamPolicyInterceptor()), + ) + p.serv = grpc.NewServer(opts...) daemonListener, jsonListener, err := listenDaemonSockets() if err != nil { @@ -145,6 +151,7 @@ func (p *program) serve(daemonListener, jsonListener *socketListener) error { } serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled) + p.policyGate.SetPolicy(serverInstance) if err := serverInstance.Start(); err != nil { return fmt.Errorf("start daemon: %w", err) } diff --git a/client/internal/ipcauth/policy.go b/client/internal/ipcauth/policy.go new file mode 100644 index 000000000..2562aa100 --- /dev/null +++ b/client/internal/ipcauth/policy.go @@ -0,0 +1,79 @@ +package ipcauth + +import ( + "context" + "sync" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type Policy interface { + SessionHolder() (Identity, bool) +} + +type PolicyGate struct { + mu sync.Mutex + policy Policy +} + +func NewPolicyGate() *PolicyGate { + return &PolicyGate{} +} + +func (g *PolicyGate) SetPolicy(p Policy) { + g.mu.Lock() + defer g.mu.Unlock() + g.policy = p +} + +func (g *PolicyGate) SessionHolder() (Identity, bool) { + g.mu.Lock() + defer g.mu.Unlock() + if g.policy == nil { + return Identity{}, false + } + return g.policy.SessionHolder() +} + +func (g *PolicyGate) StreamPolicyInterceptor() grpc.StreamServerInterceptor { + return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + if !g.authorize(ss.Context()) { + return status.Error(codes.PermissionDenied, "caller is not session owner") + } + return handler(ss.Context(), ss) + } +} + +func (g *PolicyGate) UnaryPolicyInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + if !g.authorize(ctx) { + return nil, status.Error(codes.PermissionDenied, "caller is not session owner") + } + return handler(ctx, req) + } +} + +func (g *PolicyGate) authorize(ctx context.Context) bool { + id, ok := CallerIdentity(ctx) + if !ok { + return false + } + g.mu.Lock() + if g.policy == nil { + g.mu.Unlock() + return false + } + sessionId, running := g.policy.SessionHolder() + // TODO improve logging + log.Infof("id : %v, session holder: %v", id, sessionId) + g.mu.Unlock() + // TODO windows + // TODO allow root + if running && sessionId.UID != id.UID { + return false + } + return true +} diff --git a/client/server/server.go b/client/server/server.go index 410a9d98f..b7a9c0ffd 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -35,6 +35,7 @@ import ( "github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/client/internal/ipcauth" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/updater" @@ -148,6 +149,8 @@ type Server struct { loginAttemptFn func(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error) isLoginRequiredFn func(ctx context.Context) (bool, error) + + sessionHolder ipcauth.Identity } type oauthAuthFlow struct { @@ -1083,6 +1086,14 @@ func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpR s.statusRecorder.UpdateRosenpass(s.config.RosenpassEnabled, s.config.RosenpassPermissive) s.localMetrics.Reconcile(s.config.LocalMetricsEnabled, s.config.LocalMetricsAddress) + id, ok := ipcauth.CallerIdentity(callerCtx) + if !ok { + s.mutex.Unlock() + return nil, fmt.Errorf("failed to get identity") + } + + log.Infof("setting session holder: %d", id.UID) + s.sessionHolder = id s.clientRunning = true s.clientRunningChan = make(chan struct{}) s.clientGiveUpChan = make(chan struct{}) @@ -1332,6 +1343,7 @@ func (s *Server) cleanupConnection() error { // explicitly asked for it. MDM restart does NOT go through this // path, so its clientRunning stays true. s.clientRunning = false + s.sessionHolder = ipcauth.Identity{} // Capture the engine reference before cancelling the context. // After actCancel(), the connectWithRetryRuns goroutine wakes up @@ -2695,6 +2707,15 @@ func (s *Server) authorizeAndPrepareLogin(callerCtx context.Context, msg *proto. return ctx, activeProf, nil } +func (s *Server) SessionHolder() (ipcauth.Identity, bool) { + s.mutex.Lock() + defer s.mutex.Unlock() + if !s.clientRunning { + return ipcauth.Identity{}, false + } + return s.sessionHolder, true +} + func persistLoginOverrides(activeProf *profilemanager.ActiveProfileState, managementURL string, preSharedKey *string) error { if preSharedKey != nil && *preSharedKey == "" { preSharedKey = nil