diff --git a/compat/module.go b/compat/module.go index c87afd1a..1517f2cc 100644 --- a/compat/module.go +++ b/compat/module.go @@ -52,6 +52,9 @@ func start() error { MaxDelay(selfcheckTaskRetryAfter). Schedule(time.Now().Add(selfcheckTaskRetryAfter)) + module.NewTask("clean notify thresholds", cleanNotifyThreshold). + Repeat(10 * time.Minute) + return module.RegisterEventHook( netenv.ModuleName, netenv.NetworkChangedEvent, diff --git a/compat/notify.go b/compat/notify.go index 893f60d4..9a263e7a 100644 --- a/compat/notify.go +++ b/compat/notify.go @@ -8,6 +8,7 @@ import ( "time" "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" "github.com/safing/portbase/notifications" "github.com/safing/portmaster/process" "github.com/safing/portmaster/profile" @@ -124,9 +125,6 @@ func (issue *appIssue) notify(proc *process.Process) { proc.Path, ) - // Build message. - message := strings.ReplaceAll(issue.message, "[APPNAME]", p.Name) - // Check if we already have this notification. eventID := fmt.Sprintf(issue.id, p.ID) n := notifications.Get(eventID) @@ -134,7 +132,15 @@ func (issue *appIssue) notify(proc *process.Process) { return } - // Otherwise, create a new one. + // Check if we reach the threshold to actually send a notification. + if !isOverThreshold(eventID) { + return + } + + // Build message. + message := strings.ReplaceAll(issue.message, "[APPNAME]", p.Name) + + // Create a new notification. n = ¬ifications.Notification{ EventID: eventID, Type: issue.level, @@ -171,3 +177,54 @@ func (issue *appIssue) notify(proc *process.Process) { return nil }) } + +const ( + notifyThresholdMinIncidents = 11 + notifyThresholdResetAfter = 2 * time.Minute +) + +var ( + notifyThresholds = make(map[string]*notifyThreshold) + notifyThresholdsLock sync.Mutex +) + +type notifyThreshold struct { + FirstSeen time.Time + Incidents uint +} + +func (nt *notifyThreshold) expired() bool { + return time.Now().Add(-notifyThresholdResetAfter).After(nt.FirstSeen) +} + +func isOverThreshold(id string) bool { + notifyThresholdsLock.Lock() + defer notifyThresholdsLock.Unlock() + + // Get notify threshold and check if we reach the minimum incidents. + nt, ok := notifyThresholds[id] + if ok && !nt.expired() { + nt.Incidents++ + return nt.Incidents >= notifyThresholdMinIncidents + } + + // Add new entry. + notifyThresholds[id] = ¬ifyThreshold{ + FirstSeen: time.Now(), + Incidents: 1, + } + return false +} + +func cleanNotifyThreshold(ctx context.Context, task *modules.Task) error { + notifyThresholdsLock.Lock() + defer notifyThresholdsLock.Unlock() + + for id, nt := range notifyThresholds { + if nt.expired() { + delete(notifyThresholds, id) + } + } + + return nil +}