Refactor status package to use portbase/runtime.

Refactor the status package to use portbase/runtime and
make system status readonly. Also adapts the code base
to the new portbase/notifications package.
This commit is contained in:
Patrick Pacher 2020-09-21 17:12:52 +02:00
parent 52c4cfe11d
commit a5e3f7ff37
No known key found for this signature in database
GPG key ID: E8CD2DA160925A6D
22 changed files with 527 additions and 554 deletions

View file

@ -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
}

View file

@ -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 = (&notifications.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

View file

@ -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
(&notifications.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)

View file

@ -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
}

36
status/autopilot.go Normal file
View file

@ -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()
}
}

View file

@ -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
)

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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))
}

View file

@ -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)
}

60
status/mitigation.go Normal file
View file

@ -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
}

View file

@ -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()
}

View file

@ -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()
}

93
status/provider.go Normal file
View file

@ -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())
}

42
status/records.go Normal file
View file

@ -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
}

114
status/security_level.go Normal file
View file

@ -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"
}
}

View file

@ -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))
}

View file

@ -1,11 +0,0 @@
package status
import "testing"
func TestSet(t *testing.T) {
// only test for panics
// TODO: write real tests
setSelectedSecurityLevel(0)
}

30
status/state.go Normal file
View file

@ -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))
}

View file

@ -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"
}
}

View file

@ -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())
}
}

View file

@ -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: &notifications.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
}