diff --git a/core/base/global.go b/core/base/global.go index 0414a4f2..b314797c 100644 --- a/core/base/global.go +++ b/core/base/global.go @@ -9,7 +9,6 @@ import ( "github.com/safing/portbase/dataroot" "github.com/safing/portbase/info" "github.com/safing/portbase/modules" - "github.com/safing/portbase/modules/subsystems" ) // Default Values (changeable for testing) @@ -66,8 +65,5 @@ func globalPrep() error { // set api listen address api.SetDefaultAPIListenAddress(DefaultAPIListenAddress) - // set subsystem status dir - subsystems.SetDatabaseKeySpace("core:status/subsystems") - return nil } diff --git a/firewall/prompt.go b/firewall/prompt.go index bc4f7109..b05b5297 100644 --- a/firewall/prompt.go +++ b/firewall/prompt.go @@ -46,18 +46,16 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // // do not save response to profile saveResponse = false } else { - // create new notification - n = (¬ifications.Notification{ - ID: nID, - Type: notifications.Prompt, - Expires: time.Now().Add(nTTL).Unix(), - }) + var ( + msg string + actions []notifications.Action + ) // add message and actions switch { case conn.Inbound: - n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) - n.AvailableActions = []*notifications.Action{ + msg = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) + actions = []notifications.Action{ { ID: permitServingIP, Text: "Permit", @@ -68,8 +66,8 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // }, } case conn.Entity.Domain == "": // direct connection - n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) - n.AvailableActions = []*notifications.Action{ + msg = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) + actions = []notifications.Action{ { ID: permitIP, Text: "Permit", @@ -81,11 +79,11 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // } default: // connection to domain if pkt != nil { - n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", conn.Process(), conn.Entity.Domain, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) + msg = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", conn.Process(), conn.Entity.Domain, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) } else { - n.Message = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain) + msg = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain) } - n.AvailableActions = []*notifications.Action{ + actions = []notifications.Action{ { ID: permitDomainAll, Text: "Permit all", @@ -100,8 +98,8 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // }, } } - // save new notification - n.Save() + + n = notifications.NotifyPrompt(nID, msg, actions...) } // wait for response/timeout diff --git a/nameserver/takeover.go b/nameserver/takeover.go index ecbea5cf..51da9830 100644 --- a/nameserver/takeover.go +++ b/nameserver/takeover.go @@ -47,11 +47,10 @@ func checkForConflictingService() error { // wait for a short duration for the other service to shut down time.Sleep(10 * time.Millisecond) - // notify user - (¬ifications.Notification{ - ID: "nameserver-stopped-conflicting-service", - Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid), - }).Save() + notifications.NotifyInfo( + "namserver-stopped-conflicting-service", + fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid), + ) // restart via service-worker logic return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid) diff --git a/resolver/config.go b/resolver/config.go index f17a4d91..3b697f03 100644 --- a/resolver/config.go +++ b/resolver/config.go @@ -57,19 +57,19 @@ var ( cfgOptionNameServersOrder = 0 CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers" - noAssignedNameservers status.SecurityLevelOption + noAssignedNameservers status.SecurityLevelOptionFunc cfgOptionNoAssignedNameserversOrder = 1 CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS" - noMulticastDNS status.SecurityLevelOption + noMulticastDNS status.SecurityLevelOptionFunc cfgOptionNoMulticastDNSOrder = 2 CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols" - noInsecureProtocols status.SecurityLevelOption + noInsecureProtocols status.SecurityLevelOptionFunc cfgOptionNoInsecureProtocolsOrder = 3 CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains" - dontResolveSpecialDomains status.SecurityLevelOption + dontResolveSpecialDomains status.SecurityLevelOptionFunc cfgOptionDontResolveSpecialDomainsOrder = 16 CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate" @@ -122,6 +122,7 @@ Parameters: err = config.Register(&config.Option{ Name: "DNS Server Retry Rate", + Key: CfgOptionNameserverRetryRateKey, Description: "Rate at which to retry failed DNS Servers, in seconds.", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelExpert, @@ -154,7 +155,7 @@ Parameters: if err != nil { return err } - noMulticastDNS = status.ConfigIsActiveConcurrent(CfgOptionNoMulticastDNSKey) + noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey) err = config.Register(&config.Option{ Name: "Do not use assigned Nameservers", @@ -173,7 +174,7 @@ Parameters: if err != nil { return err } - noAssignedNameservers = status.ConfigIsActiveConcurrent(CfgOptionNoAssignedNameserversKey) + noAssignedNameservers = status.SecurityLevelOption(CfgOptionNoAssignedNameserversKey) err = config.Register(&config.Option{ Name: "Do not resolve insecurely", @@ -192,7 +193,7 @@ Parameters: if err != nil { return err } - noInsecureProtocols = status.ConfigIsActiveConcurrent(CfgOptionNoInsecureProtocolsKey) + noInsecureProtocols = status.SecurityLevelOption(CfgOptionNoInsecureProtocolsKey) err = config.Register(&config.Option{ Name: "Do not resolve special domains", @@ -211,7 +212,7 @@ Parameters: if err != nil { return err } - dontResolveSpecialDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveSpecialDomainsKey) + dontResolveSpecialDomains = status.SecurityLevelOption(CfgOptionDontResolveSpecialDomainsKey) return nil } diff --git a/status/autopilot.go b/status/autopilot.go new file mode 100644 index 00000000..63cf388a --- /dev/null +++ b/status/autopilot.go @@ -0,0 +1,36 @@ +package status + +import "context" + +var runAutoPilot = make(chan struct{}) + +func triggerAutopilot() { + select { + case runAutoPilot <- struct{}{}: + default: + } +} + +func autoPilot(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + case <-runAutoPilot: + } + + selected := SelectedSecurityLevel() + mitigation := getHighestMitigationLevel() + + active := SecurityLevelNormal + if selected != SecurityLevelOff { + active = selected + } else if mitigation != SecurityLevelOff { + active = mitigation + } + + setActiveLevel(active) + + pushSystemStatus() + } +} diff --git a/status/const.go b/status/const.go deleted file mode 100644 index 96537903..00000000 --- a/status/const.go +++ /dev/null @@ -1,56 +0,0 @@ -package status - -import ( - "github.com/safing/portbase/config" -) - -// DisplayHintSecurityLevel is an external option hint for security levels. -// It's meant to be used as a value for config.DisplayHintAnnotation. -const DisplayHintSecurityLevel string = "security level" - -// Security levels -const ( - SecurityLevelOff uint8 = 0 - SecurityLevelNormal uint8 = 1 - SecurityLevelHigh uint8 = 2 - SecurityLevelExtreme uint8 = 4 - - SecurityLevelsNormalAndHigh uint8 = SecurityLevelNormal | SecurityLevelHigh - SecurityLevelsNormalAndExtreme uint8 = SecurityLevelNormal | SecurityLevelExtreme - SecurityLevelsHighAndExtreme uint8 = SecurityLevelHigh | SecurityLevelExtreme - SecurityLevelsAll uint8 = SecurityLevelNormal | SecurityLevelHigh | SecurityLevelExtreme -) - -// SecurityLevelValues defines all possible security levels. -var SecurityLevelValues = []config.PossibleValue{ - { - Name: "Normal", - Value: SecurityLevelsAll, - }, - { - Name: "High", - Value: SecurityLevelsHighAndExtreme, - }, - { - Name: "Extreme", - Value: SecurityLevelExtreme, - }, -} - -// AllSecurityLevelValues is like SecurityLevelValues but also includes Off. -var AllSecurityLevelValues = append([]config.PossibleValue{ - { - Name: "Off", - Value: SecurityLevelOff, - }, -}, - SecurityLevelValues..., -) - -// Status constants -const ( - StatusOff uint8 = 0 - StatusError uint8 = 1 - StatusWarning uint8 = 2 - StatusOk uint8 = 3 -) diff --git a/status/database.go b/status/database.go deleted file mode 100644 index 6b89bc0f..00000000 --- a/status/database.go +++ /dev/null @@ -1,59 +0,0 @@ -package status - -import ( - "context" - - "github.com/safing/portbase/database" - "github.com/safing/portbase/database/query" - "github.com/safing/portbase/database/record" -) - -const ( - statusDBKey = "core:status/status" -) - -var ( - statusDB = database.NewInterface(nil) - hook *database.RegisteredHook -) - -type statusHook struct { - database.HookBase -} - -// UsesPrePut implements the Hook interface. -func (sh *statusHook) UsesPrePut() bool { - return true -} - -// PrePut implements the Hook interface. -func (sh *statusHook) PrePut(r record.Record) (record.Record, error) { - // record is already locked! - - newStatus, err := EnsureSystemStatus(r) - if err != nil { - return nil, err - } - - // apply applicable settings - if SelectedSecurityLevel() != newStatus.SelectedSecurityLevel { - module.StartWorker("set selected security level", func(_ context.Context) error { - setSelectedSecurityLevel(newStatus.SelectedSecurityLevel) - return nil - }) - } - - // TODO: allow setting of Gate17 status (on/off) - - // return original status - return status, nil -} - -func initStatusHook() (err error) { - hook, err = database.RegisterHook(query.New(statusDBKey), &statusHook{}) - return err -} - -func stopStatusHook() error { - return hook.Cancel() -} diff --git a/status/get-config.go b/status/get-config.go deleted file mode 100644 index b216e4b2..00000000 --- a/status/get-config.go +++ /dev/null @@ -1,33 +0,0 @@ -package status - -import ( - "github.com/safing/portbase/config" -) - -type ( - // SecurityLevelOption defines the returned function by ConfigIsActive. - SecurityLevelOption func(minSecurityLevel uint8) bool -) - -func max(a, b uint8) uint8 { - if a > b { - return a - } - return b -} - -// ConfigIsActive returns whether the given security level dependent config option is on or off. -func ConfigIsActive(name string) SecurityLevelOption { - activeAtLevel := config.GetAsInt(name, int64(SecurityLevelsAll)) - return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 - } -} - -// ConfigIsActiveConcurrent returns whether the given security level dependent config option is on or off and is concurrency safe. -func ConfigIsActiveConcurrent(name string) SecurityLevelOption { - activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll)) - return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 - } -} diff --git a/status/get.go b/status/get.go deleted file mode 100644 index 24d5300c..00000000 --- a/status/get.go +++ /dev/null @@ -1,20 +0,0 @@ -package status - -import ( - "sync/atomic" -) - -var ( - activeSecurityLevel = new(uint32) - selectedSecurityLevel = new(uint32) -) - -// ActiveSecurityLevel returns the current security level. -func ActiveSecurityLevel() uint8 { - return uint8(atomic.LoadUint32(activeSecurityLevel)) -} - -// SelectedSecurityLevel returns the selected security level. -func SelectedSecurityLevel() uint8 { - return uint8(atomic.LoadUint32(selectedSecurityLevel)) -} diff --git a/status/get_test.go b/status/get_test.go deleted file mode 100644 index 10de0a85..00000000 --- a/status/get_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package status - -import "testing" - -func TestGet(t *testing.T) { - - // only test for panics - // TODO: write real tests - ActiveSecurityLevel() - SelectedSecurityLevel() - option := ConfigIsActive("invalid") - option(0) - option = ConfigIsActiveConcurrent("invalid") - option(0) - -} diff --git a/status/mitigation.go b/status/mitigation.go new file mode 100644 index 00000000..5d103eb4 --- /dev/null +++ b/status/mitigation.go @@ -0,0 +1,60 @@ +package status + +import ( + "sync" + + "github.com/safing/portbase/log" +) + +type knownThreats struct { + sync.RWMutex + // active threats and their recommended mitigation level + list map[string]uint8 +} + +var threats = &knownThreats{ + list: make(map[string]uint8), +} + +// SetMitigationLevel sets the mitigation level for id +// to mitigation. If mitigation is SecurityLevelOff the +// mitigation record will be removed. If mitigation is +// an invalid level the call to SetMitigationLevel is a +// no-op. +func SetMitigationLevel(id string, mitigation uint8) { + if !IsValidSecurityLevel(mitigation) { + log.Warningf("tried to set invalid mitigation level %d for threat %s", mitigation, id) + return + } + + defer triggerAutopilot() + + threats.Lock() + defer threats.Unlock() + if mitigation == 0 { + delete(threats.list, id) + } else { + threats.list[id] = mitigation + } +} + +// DeleteMitigationLevel deletes the mitigation level for id. +func DeleteMitigationLevel(id string) { + SetMitigationLevel(id, SecurityLevelOff) +} + +// getHighestMitigationLevel returns the highest mitigation +// level set on a threat. +func getHighestMitigationLevel() uint8 { + threats.RLock() + defer threats.RUnlock() + + var level uint8 + for _, lvl := range threats.list { + if lvl > level { + level = lvl + } + } + + return level +} diff --git a/status/module.go b/status/module.go index 12607767..2dbb13a5 100644 --- a/status/module.go +++ b/status/module.go @@ -1,9 +1,10 @@ package status import ( - "github.com/safing/portbase/database" - "github.com/safing/portbase/log" + "context" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/netenv" ) var ( @@ -11,56 +12,25 @@ var ( ) func init() { - module = modules.Register("status", nil, start, stop, "base") + module = modules.Register("status", nil, start, nil, "base") } func start() error { - err := initSystemStatus() + module.StartWorker("auto-pilot", autoPilot) + triggerAutopilot() + + err := module.RegisterEventHook( + "netenv", + netenv.OnlineStatusChangedEvent, + "update online status in system status", + func(_ context.Context, _ interface{}) error { + triggerAutopilot() + return nil + }, + ) if err != nil { return err } - err = startNetEnvHooking() - if err != nil { - return err - } - - status.Save() - - return initStatusHook() -} - -func initSystemStatus() error { - // load status from database - r, err := statusDB.Get(statusDBKey) - switch err { - case nil: - loadedStatus, err := EnsureSystemStatus(r) - if err != nil { - log.Criticalf("status: failed to unwrap system status: %s", err) - } else { - status = loadedStatus - } - case database.ErrNotFound: - // create new status - default: - log.Criticalf("status: failed to load system status: %s", err) - } - - status.Lock() - defer status.Unlock() - - // load status into atomic getters - atomicUpdateSelectedSecurityLevel(status.SelectedSecurityLevel) - - // update status - status.updateThreatMitigationLevel() - status.autopilot() - status.updateOnlineStatus() - return nil } - -func stop() error { - return stopStatusHook() -} diff --git a/status/netenv.go b/status/netenv.go deleted file mode 100644 index 8d57c615..00000000 --- a/status/netenv.go +++ /dev/null @@ -1,28 +0,0 @@ -package status - -import ( - "context" - - "github.com/safing/portmaster/netenv" -) - -// startNetEnvHooking starts the listener for online status changes. -func startNetEnvHooking() error { - return module.RegisterEventHook( - "netenv", - netenv.OnlineStatusChangedEvent, - "update online status in system status", - func(_ context.Context, _ interface{}) error { - status.Lock() - status.updateOnlineStatus() - status.Unlock() - status.Save() - return nil - }, - ) -} - -func (s *SystemStatus) updateOnlineStatus() { - s.OnlineStatus = netenv.GetOnlineStatus() - s.CaptivePortal = netenv.GetCaptivePortal() -} diff --git a/status/provider.go b/status/provider.go new file mode 100644 index 00000000..130972db --- /dev/null +++ b/status/provider.go @@ -0,0 +1,93 @@ +package status + +import ( + "fmt" + + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/runtime" + "github.com/safing/portmaster/netenv" +) + +var ( + pushUpdate runtime.PushFunc +) + +func setupRuntimeProvider() (err error) { + // register the system status getter + // + statusProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { + return []record.Record{buildSystemStatus()}, nil + }) + pushUpdate, err = runtime.Register("system/status", statusProvider) + if err != nil { + return err + } + + // register the selected security level setter + // + levelProvider := runtime.SimpleValueSetterFunc(setSelectedSecurityLevel) + _, err = runtime.Register("system/security-level", levelProvider) + if err != nil { + return err + } + + return nil +} + +// setSelectedSecurityLevel updates the selected security level +func setSelectedSecurityLevel(r record.Record) (record.Record, error) { + var upd *SelectedSecurityLevelRecord + if r.IsWrapped() { + upd = new(SelectedSecurityLevelRecord) + if err := record.Unwrap(r, upd); err != nil { + return nil, err + } + } else { + // TODO(ppacher): this can actually never happen + // as we're write-only and ValueProvider.Set() should + // only ever be called from the HTTP API (so r must be wrapped). + // Though, make sure we handle the case as well ... + var ok bool + upd, ok = r.(*SelectedSecurityLevelRecord) + if !ok { + return nil, fmt.Errorf("expected *SelectedSecurityLevelRecord but got %T", r) + } + } + + if !IsValidSecurityLevel(upd.SelectedSecurityLevel) { + return nil, fmt.Errorf("invalid security level: %d", upd.SelectedSecurityLevel) + } + + if SelectedSecurityLevel() != upd.SelectedSecurityLevel { + setSelectedLevel(upd.SelectedSecurityLevel) + triggerAutopilot() + } + + return r, nil +} + +// buildSystemStatus build a new system status record. +func buildSystemStatus() *SystemStatusRecord { + status := &SystemStatusRecord{ + ActiveSecurityLevel: ActiveSecurityLevel(), + SelectedSecurityLevel: SelectedSecurityLevel(), + ThreatMitigationLevel: getHighestMitigationLevel(), + CaptivePortal: netenv.GetCaptivePortal(), + OnlineStatus: netenv.GetOnlineStatus(), + } + + status.CreateMeta() + status.SetKey("runtime:system/status") + + return status +} + +// pushSystemStatus pushes a new system status via +// the runtime database. +func pushSystemStatus() { + if pushUpdate == nil { + return + } + + pushUpdate(buildSystemStatus()) +} diff --git a/status/records.go b/status/records.go new file mode 100644 index 00000000..73801c62 --- /dev/null +++ b/status/records.go @@ -0,0 +1,42 @@ +package status + +import ( + "sync" + + "github.com/safing/portbase/database/record" + "github.com/safing/portmaster/netenv" +) + +// SystemStatusRecord describes the overall status of the Portmaster. +// It's a read-only record exposed via runtime:system/status. +type SystemStatusRecord struct { + record.Base + sync.Mutex + + // ActiveSecurityLevel holds the currently + // active security level. + ActiveSecurityLevel uint8 + // SelectedSecurityLevel holds the security level + // as selected by the user. + SelectedSecurityLevel uint8 + // ThreatMitigationLevel holds the security level + // as selected by the auto-pilot. + ThreatMitigationLevel uint8 + // OnlineStatus holds the current online status as + // seen by the netenv package. + OnlineStatus netenv.OnlineStatus + // CaptivePortal holds all information about the captive + // portal of the network the portmaster is currently + // connected to, if any. + CaptivePortal *netenv.CaptivePortal +} + +// SelectedSecurityLevelRecord is used as a dummy record.Record +// to provide a simply runtime-configuration for the user. +// It is write-only and exposed at runtime:system/security-level +type SelectedSecurityLevelRecord struct { + record.Base + sync.Mutex + + SelectedSecurityLevel uint8 +} diff --git a/status/security_level.go b/status/security_level.go new file mode 100644 index 00000000..ea8badb7 --- /dev/null +++ b/status/security_level.go @@ -0,0 +1,114 @@ +package status + +import "github.com/safing/portbase/config" + +type ( + // SecurityLevelOptionFunc can be called with a minimum security level + // and returns whether or not a given security option is enabled or + // not. + // Use SecurityLevelOption() to get a SecurityLevelOptionFunc for a + // specific option. + SecurityLevelOptionFunc func(minSecurityLevel uint8) bool +) + +// DisplayHintSecurityLevel is an external option hint for security levels. +// It's meant to be used as a value for config.DisplayHintAnnotation. +const DisplayHintSecurityLevel string = "security level" + +// Security levels +const ( + SecurityLevelOff uint8 = 0 + SecurityLevelNormal uint8 = 1 + SecurityLevelHigh uint8 = 2 + SecurityLevelExtreme uint8 = 4 + + SecurityLevelsNormalAndHigh uint8 = SecurityLevelNormal | SecurityLevelHigh + SecurityLevelsNormalAndExtreme uint8 = SecurityLevelNormal | SecurityLevelExtreme + SecurityLevelsHighAndExtreme uint8 = SecurityLevelHigh | SecurityLevelExtreme + SecurityLevelsAll uint8 = SecurityLevelNormal | SecurityLevelHigh | SecurityLevelExtreme +) + +// SecurityLevelValues defines all possible security levels. +var SecurityLevelValues = []config.PossibleValue{ + { + Name: "Normal", + Value: SecurityLevelsAll, + }, + { + Name: "High", + Value: SecurityLevelsHighAndExtreme, + }, + { + Name: "Extreme", + Value: SecurityLevelExtreme, + }, +} + +// AllSecurityLevelValues is like SecurityLevelValues but also includes Off. +var AllSecurityLevelValues = append([]config.PossibleValue{ + { + Name: "Off", + Value: SecurityLevelOff, + }, +}, + SecurityLevelValues..., +) + +// IsValidSecurityLevel returns true if level is a valid, +// single security level. Level is also invalid if it's a +// bitmask with more that one security level set. +func IsValidSecurityLevel(level uint8) bool { + return level == SecurityLevelOff || + level == SecurityLevelNormal || + level == SecurityLevelHigh || + level == SecurityLevelExtreme +} + +// IsValidSecurityLevelMask returns true if level is a valid +// security level mask. It's like IsValidSecurityLevel but +// also allows bitmask combinations. +func IsValidSecurityLevelMask(level uint8) bool { + return level <= 7 +} + +func max(a, b uint8) uint8 { + if a > b { + return a + } + return b +} + +// SecurityLevelOption returns a function to check if the option +// identified by name is active at a given minimum security level. +// The returned function is safe for concurrent use with configuration +// updates. +func SecurityLevelOption(name string) SecurityLevelOptionFunc { + activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll)) + return func(minSecurityLevel uint8) bool { + return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 + } +} + +// SecurityLevelString returns the given security level as a string. +func SecurityLevelString(level uint8) string { + switch level { + case SecurityLevelOff: + return "Off" + case SecurityLevelNormal: + return "Normal" + case SecurityLevelHigh: + return "High" + case SecurityLevelExtreme: + return "Extreme" + case SecurityLevelsNormalAndHigh: + return "Normal and High" + case SecurityLevelsNormalAndExtreme: + return "Normal and Extreme" + case SecurityLevelsHighAndExtreme: + return "High and Extreme" + case SecurityLevelsAll: + return "Normal, High and Extreme" + default: + return "INVALID" + } +} diff --git a/status/set.go b/status/set.go deleted file mode 100644 index 34899881..00000000 --- a/status/set.go +++ /dev/null @@ -1,54 +0,0 @@ -package status - -import ( - "sync/atomic" - - "github.com/safing/portbase/log" -) - -// autopilot automatically adjusts the security level as needed. -func (s *SystemStatus) autopilot() { - // check if users is overruling - if s.SelectedSecurityLevel > SecurityLevelOff { - s.ActiveSecurityLevel = s.SelectedSecurityLevel - atomicUpdateActiveSecurityLevel(s.SelectedSecurityLevel) - return - } - - // update active security level - switch s.ThreatMitigationLevel { - case SecurityLevelOff: - s.ActiveSecurityLevel = SecurityLevelNormal - atomicUpdateActiveSecurityLevel(SecurityLevelNormal) - case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: - s.ActiveSecurityLevel = s.ThreatMitigationLevel - atomicUpdateActiveSecurityLevel(s.ThreatMitigationLevel) - default: - log.Errorf("status: threat mitigation level is set to invalid value: %d", s.ThreatMitigationLevel) - } -} - -// setSelectedSecurityLevel sets the selected security level. -func setSelectedSecurityLevel(level uint8) { - switch level { - case SecurityLevelOff, SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: - status.Lock() - - status.SelectedSecurityLevel = level - atomicUpdateSelectedSecurityLevel(level) - status.autopilot() - - status.Unlock() - status.Save() - default: - log.Errorf("status: tried to set selected security level to invalid value: %d", level) - } -} - -func atomicUpdateActiveSecurityLevel(level uint8) { - atomic.StoreUint32(activeSecurityLevel, uint32(level)) -} - -func atomicUpdateSelectedSecurityLevel(level uint8) { - atomic.StoreUint32(selectedSecurityLevel, uint32(level)) -} diff --git a/status/set_test.go b/status/set_test.go deleted file mode 100644 index 7bb70f41..00000000 --- a/status/set_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package status - -import "testing" - -func TestSet(t *testing.T) { - - // only test for panics - // TODO: write real tests - setSelectedSecurityLevel(0) - -} diff --git a/status/state.go b/status/state.go new file mode 100644 index 00000000..a3fb079a --- /dev/null +++ b/status/state.go @@ -0,0 +1,30 @@ +package status + +import ( + "sync/atomic" +) + +var ( + activeLevel = new(uint32) + selectedLevel = new(uint32) +) + +func setActiveLevel(lvl uint8) { + atomic.StoreUint32(activeLevel, uint32(lvl)) +} + +func setSelectedLevel(lvl uint8) { + atomic.StoreUint32(selectedLevel, uint32(lvl)) +} + +// ActiveSecurityLevel returns the currently active security +// level. +func ActiveSecurityLevel() uint8 { + return uint8(atomic.LoadUint32(activeLevel)) +} + +// SelectedSecurityLevel returns the security level as selected +// by the user. +func SelectedSecurityLevel() uint8 { + return uint8(atomic.LoadUint32(selectedLevel)) +} diff --git a/status/status.go b/status/status.go deleted file mode 100644 index fb2ad0b9..00000000 --- a/status/status.go +++ /dev/null @@ -1,113 +0,0 @@ -package status - -import ( - "context" - "fmt" - "sync" - - "github.com/safing/portmaster/netenv" - - "github.com/safing/portbase/database/record" - "github.com/safing/portbase/log" -) - -var ( - status *SystemStatus -) - -func init() { - status = &SystemStatus{ - Threats: make(map[string]*Threat), - } - status.SetKey(statusDBKey) -} - -// SystemStatus saves basic information about the current system status. -//nolint:maligned // TODO -type SystemStatus struct { - record.Base - sync.Mutex - - ActiveSecurityLevel uint8 - SelectedSecurityLevel uint8 - - OnlineStatus netenv.OnlineStatus - CaptivePortal *netenv.CaptivePortal - - ThreatMitigationLevel uint8 - Threats map[string]*Threat -} - -// SaveAsync saves the SystemStatus to the database asynchronously. -func (s *SystemStatus) SaveAsync() { - module.StartWorker("save system status", func(_ context.Context) error { - s.Save() - return nil - }) -} - -// Save saves the SystemStatus to the database. -func (s *SystemStatus) Save() { - err := statusDB.Put(s) - if err != nil { - log.Errorf("status: could not save status to database: %s", err) - } -} - -// EnsureSystemStatus ensures that the given record is of type SystemStatus and unwraps it, if needed. -func EnsureSystemStatus(r record.Record) (*SystemStatus, error) { - // unwrap - if r.IsWrapped() { - // only allocate a new struct, if we need it - new := &SystemStatus{} - err := record.Unwrap(r, new) - if err != nil { - return nil, err - } - return new, nil - } - - // or adjust type - new, ok := r.(*SystemStatus) - if !ok { - return nil, fmt.Errorf("record not of type *SystemStatus, but %T", r) - } - return new, nil -} - -// FmtActiveSecurityLevel returns the current security level as a string. -func FmtActiveSecurityLevel() string { - status.Lock() - mitigationLevel := status.ThreatMitigationLevel - status.Unlock() - active := ActiveSecurityLevel() - s := FmtSecurityLevel(active) - if mitigationLevel > 0 && active != mitigationLevel { - s += "*" - } - return s -} - -// FmtSecurityLevel returns the given security level as a string. -func FmtSecurityLevel(level uint8) string { - switch level { - case SecurityLevelOff: - return "Off" - case SecurityLevelNormal: - return "Normal" - case SecurityLevelHigh: - return "High" - case SecurityLevelExtreme: - return "Extreme" - case SecurityLevelsNormalAndHigh: - return "Normal and High" - case SecurityLevelsNormalAndExtreme: - return "Normal and Extreme" - case SecurityLevelsHighAndExtreme: - return "High and Extreme" - case SecurityLevelsAll: - return "Normal, High and Extreme" - default: - return "INVALID" - } -} diff --git a/status/status_test.go b/status/status_test.go deleted file mode 100644 index 818b5801..00000000 --- a/status/status_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package status - -import "testing" - -func TestStatus(t *testing.T) { - - setSelectedSecurityLevel(SecurityLevelOff) - if FmtActiveSecurityLevel() != "Normal" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelNormal) - AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelHigh}) - if FmtActiveSecurityLevel() != "Normal*" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelHigh) - if FmtActiveSecurityLevel() != "High" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelHigh) - AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelExtreme}) - if FmtActiveSecurityLevel() != "High*" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelExtreme) - if FmtActiveSecurityLevel() != "Extreme" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - -} diff --git a/status/threat.go b/status/threat.go index 632ce835..4b70bfab 100644 --- a/status/threat.go +++ b/status/threat.go @@ -1,73 +1,131 @@ package status import ( - "strings" - "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/notifications" ) -// Threat describes a detected threat. +// Threat represents a threat to the system. +// A threat is basically a notification with strong +// typed EventData. Use the methods expored on Threat +// to manipulate the EventData field and push updates +// of the notification. +// Do not use EventData directly! type Threat struct { - ID string // A unique ID chosen by reporting module (eg. modulePrefix-incident) to periodically check threat existence - Name string // Descriptive (human readable) name for detected threat - Description string // Simple description - AdditionalData interface{} // Additional data a module wants to make available for the user - MitigationLevel uint8 // Recommended Security Level to switch to for mitigation - Started int64 - Ended int64 - // TODO: add locking + *notifications.Notification } -// AddOrUpdateThreat adds or updates a new threat in the system status. -func AddOrUpdateThreat(new *Threat) { - status.Lock() - defer status.Unlock() - - status.Threats[new.ID] = new - status.updateThreatMitigationLevel() - status.autopilot() - - status.SaveAsync() +// ThreatPayload holds threat related information. +type ThreatPayload struct { + // MitigationLevel holds the recommended security + // level to mitigate the threat. + MitigationLevel uint8 + // Started holds the UNIX epoch timestamp in seconds + // at which the threat has been detected the first time. + Started int64 + // Ended holds the UNIX epoch timestamp in seconds + // at which the threat has been detected the last time. + Ended int64 + // Data may holds threat-specific data. + Data interface{} } -// DeleteThreat deletes a threat from the system status. -func DeleteThreat(id string) { - status.Lock() - defer status.Unlock() +// NewThreat returns a new threat. Note that the +// threat only gets published once Publish is called. +// +// Example: +// +// threat := NewThreat("portscan", "Someone is scanning you"). +// SetData(portscanResult). +// SetMitigationLevel(SecurityLevelExtreme). +// Publish() +// +// // Once you're done, delete the threat +// threat.Delete().Publish() +// +func NewThreat(id, msg string) *Threat { + t := &Threat{ + Notification: ¬ifications.Notification{ + EventID: id, + Message: msg, + Type: notifications.Warning, + State: notifications.Active, + }, + } + t.threatData().Started = time.Now().Unix() - delete(status.Threats, id) - status.updateThreatMitigationLevel() - status.autopilot() - - status.SaveAsync() + return t } -// GetThreats returns all threats who's IDs are prefixed by the given string, and also a locker for editing them. -func GetThreats(idPrefix string) ([]*Threat, sync.Locker) { - status.Lock() - defer status.Unlock() +// SetData sets the data member of the threat payload. +func (t *Threat) SetData(data interface{}) *Threat { + t.Lock() + defer t.Unlock() - var exportedThreats []*Threat - for id, threat := range status.Threats { - if strings.HasPrefix(id, idPrefix) { - exportedThreats = append(exportedThreats, threat) - } + t.threatData().Data = data + return t +} + +// SetMitigationLevel sets the mitigation level of the +// threat data. +func (t *Threat) SetMitigationLevel(lvl uint8) *Threat { + t.Lock() + defer t.Unlock() + + t.threatData().MitigationLevel = lvl + return t +} + +// Delete sets the ended timestamp of the threat. +func (t *Threat) Delete() *Threat { + t.Lock() + defer t.Unlock() + + t.threatData().Ended = time.Now().Unix() + + return t +} + +// Payload returns a copy of the threat payload. +func (t *Threat) Payload() ThreatPayload { + t.Lock() + defer t.Unlock() + + return *t.threatData() // creates a copy +} + +// Publish publishes the current threat. +// Publish should always be called when changes to +// the threat are recorded. +func (t *Threat) Publish() *Threat { + data := t.Payload() + if data.Ended > 0 { + DeleteMitigationLevel(t.EventID) + } else { + SetMitigationLevel(t.EventID, data.MitigationLevel) } - return exportedThreats, &status.Mutex + t.Save() + + return t } -func (s *SystemStatus) updateThreatMitigationLevel() { - // get highest mitigationLevel - var mitigationLevel uint8 - for _, threat := range s.Threats { - switch threat.MitigationLevel { - case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: - if threat.MitigationLevel > mitigationLevel { - mitigationLevel = threat.MitigationLevel - } - } +// threatData returns the threat payload associated with this +// threat. If not data has been created yet a new ThreatPayload +// is attached to t and returned. The caller must make sure to +// hold appropriate locks when working with the returned payload. +func (t *Threat) threatData() *ThreatPayload { + if t.EventData == nil { + t.EventData = new(ThreatPayload) } - // set new ThreatMitigationLevel - s.ThreatMitigationLevel = mitigationLevel + payload, ok := t.EventData.(*ThreatPayload) + if !ok { + log.Warningf("unexpected type %T in thread notification payload", t.EventData) + return new(ThreatPayload) + } + + return payload }