diff --git a/compat/module.go b/compat/module.go index 2cc6eca7..1517f2cc 100644 --- a/compat/module.go +++ b/compat/module.go @@ -28,6 +28,10 @@ var ( selfcheckFails int ) +// selfcheckFailThreshold holds the threshold of how many times the selfcheck +// must fail before it is reported. +const selfcheckFailThreshold = 5 + func init() { module = modules.Register("compat", prep, start, stop, "base", "network", "interception", "netenv", "notifications") @@ -48,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, @@ -82,7 +89,7 @@ func selfcheckTaskFunc(ctx context.Context, task *modules.Task) error { selfcheckFails++ log.Errorf("compat: %s", err) - if selfcheckFails >= 3 { + if selfcheckFails >= selfcheckFailThreshold { issue.notify(err) } diff --git a/compat/notify.go b/compat/notify.go index cc317d21..04beeff7 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" @@ -42,10 +43,9 @@ var ( } secureDNSBypassIssue = &appIssue{ - id: "compat:secure-dns-bypass-%s", - title: "Detected %s Bypass Attempt", - message: `[APPNAME] is bypassing Portmaster's firewall functions through its Secure DNS resolver. Portmaster can no longer protect or filter connections coming from [APPNAME]. Disable Secure DNS within [APPNAME] to restore functionality. -Rest assured that Portmaster already handles Secure DNS for your whole device.`, + id: "compat:secure-dns-bypass-%s", + title: "Blocked Bypass Attempt by %s", + message: `[APPNAME] is using its own Secure DNS resolver, which would bypass Portmaster's firewall protections. If [APPNAME] experiences problems, disable Secure DNS within [APPNAME] to restore functionality. Rest assured that Portmaster handles Secure DNS for your whole device, including [APPNAME].`, // TODO: Add this when the new docs page is finished: // , or [find out about other options](link to new docs page) level: notifications.Warning, @@ -124,9 +124,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 +131,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 +176,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 +} diff --git a/compat/selfcheck.go b/compat/selfcheck.go index 4aee02a7..1931b70e 100644 --- a/compat/selfcheck.go +++ b/compat/selfcheck.go @@ -28,12 +28,12 @@ var ( systemIntegrationCheckDialNet = fmt.Sprintf("ip4:%d", uint8(SystemIntegrationCheckProtocol)) systemIntegrationCheckDialIP = SystemIntegrationCheckDstIP.String() systemIntegrationCheckPackets = make(chan packet.Packet, 1) - systemIntegrationCheckWaitDuration = 10 * time.Second + systemIntegrationCheckWaitDuration = 30 * time.Second // DNSCheckInternalDomainScope is the domain scope to use for dns checks. DNSCheckInternalDomainScope = ".self-check." + resolver.InternalSpecialUseDomain dnsCheckReceivedDomain = make(chan string, 1) - dnsCheckWaitDuration = 10 * time.Second + dnsCheckWaitDuration = 30 * time.Second dnsCheckAnswerLock sync.Mutex dnsCheckAnswer net.IP ) diff --git a/network/connection.go b/network/connection.go index 31219259..2756cffa 100644 --- a/network/connection.go +++ b/network/connection.go @@ -580,10 +580,7 @@ func (conn *Connection) SetFirewallHandler(handler FirewallHandler) { conn.pktQueue = make(chan packet.Packet, 1000) // start handling - module.StartWorker("packet handler", func(ctx context.Context) error { - conn.packetHandler() - return nil - }) + module.StartWorker("packet handler", conn.packetHandlerWorker) } conn.firewallHandler = handler } @@ -608,35 +605,46 @@ func (conn *Connection) HandlePacket(pkt packet.Packet) { } } -// packetHandler sequentially handles queued packets. -func (conn *Connection) packetHandler() { - for pkt := range conn.pktQueue { - if pkt == nil { - return +// packetHandlerWorker sequentially handles queued packets. +func (conn *Connection) packetHandlerWorker(ctx context.Context) error { + for { + select { + case pkt := <-conn.pktQueue: + if pkt == nil { + return nil + } + packetHandlerHandleConn(conn, pkt) + + case <-ctx.Done(): + conn.Lock() + defer conn.Unlock() + conn.firewallHandler = nil + return nil } - // get handler - conn.Lock() + } +} - // execute handler or verdict - if conn.firewallHandler != nil { - conn.firewallHandler(conn, pkt) - } else { - defaultFirewallHandler(conn, pkt) - } +func packetHandlerHandleConn(conn *Connection, pkt packet.Packet) { + conn.Lock() + defer conn.Unlock() - // log verdict - log.Tracer(pkt.Ctx()).Infof("filter: connection %s %s: %s", conn, conn.Verdict.Verb(), conn.Reason.Msg) - // submit trace logs - log.Tracer(pkt.Ctx()).Submit() + // Handle packet with appropriate handler. + if conn.firewallHandler != nil { + conn.firewallHandler(conn, pkt) + } else { + defaultFirewallHandler(conn, pkt) + } - // save does not touch any changing data - // must not be locked, will deadlock with cleaner functions - if conn.saveWhenFinished { - conn.saveWhenFinished = false - conn.Save() - } + // Log verdict. + log.Tracer(pkt.Ctx()).Infof("filter: connection %s %s: %s", conn, conn.Verdict.Verb(), conn.Reason.Msg) + // Submit trace logs. + log.Tracer(pkt.Ctx()).Submit() - conn.Unlock() + // Save() itself does not touch any changing data. + // Must not be locked - would deadlock with cleaner functions. + if conn.saveWhenFinished { + conn.saveWhenFinished = false + conn.Save() } } diff --git a/profile/config.go b/profile/config.go index b89f3777..987594d4 100644 --- a/profile/config.go +++ b/profile/config.go @@ -575,7 +575,9 @@ The lists are automatically updated every hour using incremental updates. err = config.Register(&config.Option{ Name: "Block Bypassing", Key: CfgOptionPreventBypassingKey, - Description: `Prevent apps from bypassing the privacy filter. + Description: `Prevent apps from bypassing Portmaster's privacy protections. +If Block Bypassing is disabled, Portmaster can no longer protect you or filter connections from the affected applications. + Current Features: - Disable Firefox' internal DNS-over-HTTPs resolver - Block direct access to public DNS resolvers