Improve status module

This commit is contained in:
Daniel 2019-01-29 16:20:06 +01:00
parent e8b5cedc9d
commit 321f3feec5
13 changed files with 342 additions and 107 deletions

View file

@ -135,7 +135,7 @@ func EnsureProfile(r record.Record) (*Profile, error) {
// or adjust type // or adjust type
new, ok := r.(*Profile) new, ok := r.(*Profile)
if !ok { if !ok {
return nil, fmt.Errorf("record not of type *Example, but %T", r) return nil, fmt.Errorf("record not of type *Profile, but %T", r)
} }
return new, nil return new, nil
} }

View file

@ -31,7 +31,7 @@ func updateListener(sub *database.Subscription) {
profile, err := EnsureProfile(r) profile, err := EnsureProfile(r)
if err != nil { if err != nil {
log.Errorf("profile: received update for special profile, but could not read: %s", err) log.Errorf("profile: received update for profile, but could not read: %s", err)
continue continue
} }

47
status/database.go Normal file
View file

@ -0,0 +1,47 @@
package status
import (
"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) {
newStatus, err := EnsureSystemStatus(r)
if err != nil {
return nil, err
}
newStatus.Lock()
defer newStatus.Unlock()
// apply applicable settings
setSelectedSecurityLevel(newStatus.SelectedSecurityLevel)
// 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
}

View file

@ -20,7 +20,7 @@ func max(a, b uint8) uint8 {
func ConfigIsActive(name string) SecurityLevelOption { func ConfigIsActive(name string) SecurityLevelOption {
activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic)) activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic))
return func(minSecurityLevel uint8) bool { return func(minSecurityLevel uint8) bool {
return uint8(activeAtLevel()) <= max(CurrentSecurityLevel(), minSecurityLevel) return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel)
} }
} }
@ -28,6 +28,6 @@ func ConfigIsActive(name string) SecurityLevelOption {
func ConfigIsActiveConcurrent(name string) SecurityLevelOption { func ConfigIsActiveConcurrent(name string) SecurityLevelOption {
activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelDynamic)) activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelDynamic))
return func(minSecurityLevel uint8) bool { return func(minSecurityLevel uint8) bool {
return uint8(activeAtLevel()) <= max(CurrentSecurityLevel(), minSecurityLevel) return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel)
} }
} }

View file

@ -5,32 +5,29 @@ import (
) )
var ( var (
currentSecurityLevel *uint32 activeSecurityLevel *uint32
selectedSecurityLevel *uint32 selectedSecurityLevel *uint32
threatLevel *uint32
portmasterStatus *uint32 portmasterStatus *uint32
gate17Status *uint32 gate17Status *uint32
) )
func init() { func init() {
var ( var (
currentSecurityLevelValue uint32 activeSecurityLevelValue uint32
selectedSecurityLevelValue uint32 selectedSecurityLevelValue uint32
threatLevelValue uint32
portmasterStatusValue uint32 portmasterStatusValue uint32
gate17StatusValue uint32 gate17StatusValue uint32
) )
currentSecurityLevel = &currentSecurityLevelValue activeSecurityLevel = &activeSecurityLevelValue
selectedSecurityLevel = &selectedSecurityLevelValue selectedSecurityLevel = &selectedSecurityLevelValue
threatLevel = &threatLevelValue
portmasterStatus = &portmasterStatusValue portmasterStatus = &portmasterStatusValue
gate17Status = &gate17StatusValue gate17Status = &gate17StatusValue
} }
// CurrentSecurityLevel returns the current security level. // ActiveSecurityLevel returns the current security level.
func CurrentSecurityLevel() uint8 { func ActiveSecurityLevel() uint8 {
return uint8(atomic.LoadUint32(currentSecurityLevel)) return uint8(atomic.LoadUint32(activeSecurityLevel))
} }
// SelectedSecurityLevel returns the selected security level. // SelectedSecurityLevel returns the selected security level.
@ -38,11 +35,6 @@ func SelectedSecurityLevel() uint8 {
return uint8(atomic.LoadUint32(selectedSecurityLevel)) return uint8(atomic.LoadUint32(selectedSecurityLevel))
} }
// ThreatLevel returns the current threat level.
func ThreatLevel() uint8 {
return uint8(atomic.LoadUint32(threatLevel))
}
// PortmasterStatus returns the current Portmaster status. // PortmasterStatus returns the current Portmaster status.
func PortmasterStatus() uint8 { func PortmasterStatus() uint8 {
return uint8(atomic.LoadUint32(portmasterStatus)) return uint8(atomic.LoadUint32(portmasterStatus))

View file

@ -5,9 +5,9 @@ import "testing"
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
// only test for panics // only test for panics
CurrentSecurityLevel() // TODO: write real tests
ActiveSecurityLevel()
SelectedSecurityLevel() SelectedSecurityLevel()
ThreatLevel()
PortmasterStatus() PortmasterStatus()
Gate17Status() Gate17Status()
option := ConfigIsActive("invalid") option := ConfigIsActive("invalid")

View file

@ -1,25 +1,59 @@
package status package status
import ( import (
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/Safing/portbase/modules" "github.com/Safing/portbase/modules"
) )
var (
shutdownSignal = make(chan struct{})
)
func init() { func init() {
modules.Register("status", prep, nil, nil) modules.Register("status", nil, start, stop)
} }
func prep() error { func start() error {
var loadedStatus *SystemStatus
if CurrentSecurityLevel() == SecurityLevelOff { // load status from database
log.Infof("switching to default active security level: dynamic") r, err := statusDB.Get(statusDBKey)
SetCurrentSecurityLevel(SecurityLevelDynamic) switch err {
case nil:
loadedStatus, err = EnsureSystemStatus(r)
if err != nil {
log.Criticalf("status: failed to unwrap system status: %s", err)
loadedStatus = nil
}
case database.ErrNotFound:
// create new status
default:
log.Criticalf("status: failed to load system status: %s", err)
} }
if SelectedSecurityLevel() == SecurityLevelOff { // activate loaded status, if available
log.Infof("switching to default selected security level: dynamic") if loadedStatus != nil {
SetSelectedSecurityLevel(SecurityLevelDynamic) status = loadedStatus
} }
status.Lock()
defer status.Unlock()
// load status into atomic getters
atomicUpdateSelectedSecurityLevel(status.SelectedSecurityLevel)
atomicUpdatePortmasterStatus(status.PortmasterStatus)
atomicUpdateGate17Status(status.Gate17Status)
// update status
status.updateThreatMitigationLevel()
status.autopilot()
go status.Save()
return initStatusHook()
}
func stop() error {
close(shutdownSignal)
return nil return nil
} }

View file

@ -1,61 +1,93 @@
package status package status
import "sync/atomic" import (
"sync/atomic"
// SetCurrentSecurityLevel sets the current security level. "github.com/Safing/portbase/log"
func SetCurrentSecurityLevel(level uint8) { )
sysStatusLock.Lock()
defer sysStatusLock.Unlock() // autopilot automatically adjusts the security level as needed
sysStatus.CurrentSecurityLevel = level func (s *SystemStatus) autopilot() {
atomicUpdateCurrentSecurityLevel(level) // 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 = SecurityLevelDynamic
atomicUpdateActiveSecurityLevel(SecurityLevelDynamic)
case SecurityLevelDynamic, SecurityLevelSecure, SecurityLevelFortress:
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. // setSelectedSecurityLevel sets the selected security level.
func SetSelectedSecurityLevel(level uint8) { func setSelectedSecurityLevel(level uint8) {
sysStatusLock.Lock() switch level {
defer sysStatusLock.Unlock() case SecurityLevelOff, SecurityLevelDynamic, SecurityLevelSecure, SecurityLevelFortress:
sysStatus.SelectedSecurityLevel = level status.Lock()
atomicUpdateSelectedSecurityLevel(level) defer status.Unlock()
}
// SetThreatLevel sets the current threat level. status.SelectedSecurityLevel = level
func SetThreatLevel(level uint8) { atomicUpdateSelectedSecurityLevel(level)
sysStatusLock.Lock() status.autopilot()
defer sysStatusLock.Unlock()
sysStatus.ThreatLevel = level go status.Save()
atomicUpdateThreatLevel(level) default:
log.Errorf("status: tried to set selected security level to invalid value: %d", level)
}
} }
// SetPortmasterStatus sets the current Portmaster status. // SetPortmasterStatus sets the current Portmaster status.
func SetPortmasterStatus(status uint8) { func SetPortmasterStatus(pmStatus uint8, msg string) {
sysStatusLock.Lock() switch pmStatus {
defer sysStatusLock.Unlock() case StatusOff, StatusError, StatusWarning, StatusOk:
sysStatus.PortmasterStatus = status status.Lock()
atomicUpdatePortmasterStatus(status) defer status.Unlock()
status.PortmasterStatus = pmStatus
status.PortmasterStatusMsg = msg
atomicUpdatePortmasterStatus(pmStatus)
go status.Save()
default:
log.Errorf("status: tried to set portmaster to invalid status: %d", status)
}
} }
// SetGate17Status sets the current Gate17 status. // SetGate17Status sets the current Gate17 status.
func SetGate17Status(status uint8) { func SetGate17Status(g17Status uint8, msg string) {
sysStatusLock.Lock() switch g17Status {
defer sysStatusLock.Unlock() case StatusOff, StatusError, StatusWarning, StatusOk:
sysStatus.Gate17Status = status status.Lock()
atomicUpdateGate17Status(status) defer status.Unlock()
status.Gate17Status = g17Status
status.Gate17StatusMsg = msg
atomicUpdateGate17Status(g17Status)
go status.Save()
default:
log.Errorf("status: tried to set gate17 to invalid status: %d", status)
}
} }
// update functions for atomic stuff // update functions for atomic stuff
func atomicUpdateActiveSecurityLevel(level uint8) {
func atomicUpdateCurrentSecurityLevel(level uint8) { atomic.StoreUint32(activeSecurityLevel, uint32(level))
atomic.StoreUint32(currentSecurityLevel, uint32(level))
} }
func atomicUpdateSelectedSecurityLevel(level uint8) { func atomicUpdateSelectedSecurityLevel(level uint8) {
atomic.StoreUint32(selectedSecurityLevel, uint32(level)) atomic.StoreUint32(selectedSecurityLevel, uint32(level))
} }
func atomicUpdateThreatLevel(level uint8) {
atomic.StoreUint32(threatLevel, uint32(level))
}
func atomicUpdatePortmasterStatus(status uint8) { func atomicUpdatePortmasterStatus(status uint8) {
atomic.StoreUint32(portmasterStatus, uint32(status)) atomic.StoreUint32(portmasterStatus, uint32(status))
} }

View file

@ -5,10 +5,9 @@ import "testing"
func TestSet(t *testing.T) { func TestSet(t *testing.T) {
// only test for panics // only test for panics
SetCurrentSecurityLevel(0) // TODO: write real tests
SetSelectedSecurityLevel(0) setSelectedSecurityLevel(0)
SetThreatLevel(0) SetPortmasterStatus(0, "")
SetPortmasterStatus(0) SetGate17Status(0, "")
SetGate17Status(0)
} }

View file

@ -1,38 +1,81 @@
package status package status
import "sync" import (
"fmt"
"sync"
"github.com/Safing/portbase/database/record"
"github.com/Safing/portbase/log"
)
var ( var (
sysStatus *SystemStatus status *SystemStatus
sysStatusLock sync.RWMutex
) )
func init() { func init() {
sysStatus = &SystemStatus{} status = &SystemStatus{
Threats: make(map[string]*Threat),
}
status.SetKey(statusDBKey)
} }
// SystemStatus saves basic information about the current system status. // SystemStatus saves basic information about the current system status.
type SystemStatus struct { type SystemStatus struct {
// database.Base record.Base
CurrentSecurityLevel uint8 sync.Mutex
ActiveSecurityLevel uint8
SelectedSecurityLevel uint8 SelectedSecurityLevel uint8
ThreatLevel uint8 `json:",omitempty" bson:",omitempty"` PortmasterStatus uint8
ThreatReason string `json:",omitempty" bson:",omitempty"` PortmasterStatusMsg string
PortmasterStatus uint8 `json:",omitempty" bson:",omitempty"` Gate17Status uint8
PortmasterStatusMsg string `json:",omitempty" bson:",omitempty"` Gate17StatusMsg string
Gate17Status uint8 `json:",omitempty" bson:",omitempty"` ThreatMitigationLevel uint8
Gate17StatusMsg string `json:",omitempty" bson:",omitempty"` Threats map[string]*Threat
UpdateStatus string
} }
// FmtCurrentSecurityLevel returns the current security level as a string. // Save saves the SystemStatus to the database
func FmtCurrentSecurityLevel() string { func (s *SystemStatus) Save() {
current := CurrentSecurityLevel() err := statusDB.Put(s)
selected := SelectedSecurityLevel() if err != nil {
s := FmtSecurityLevel(current) log.Errorf("status: could not save status to database: %s", err)
if current != selected { }
}
// 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 += "*" s += "*"
} }
return s return s

View file

@ -4,33 +4,31 @@ import "testing"
func TestStatus(t *testing.T) { func TestStatus(t *testing.T) {
SetCurrentSecurityLevel(SecurityLevelOff) setSelectedSecurityLevel(SecurityLevelOff)
SetSelectedSecurityLevel(SecurityLevelOff) if FmtActiveSecurityLevel() != "Dynamic" {
if FmtCurrentSecurityLevel() != "Off" { t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
t.Error("unexpected string representation")
} }
SetCurrentSecurityLevel(SecurityLevelDynamic) setSelectedSecurityLevel(SecurityLevelDynamic)
SetSelectedSecurityLevel(SecurityLevelDynamic) AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelSecure})
if FmtCurrentSecurityLevel() != "Dynamic" { if FmtActiveSecurityLevel() != "Dynamic*" {
t.Error("unexpected string representation") t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
} }
SetCurrentSecurityLevel(SecurityLevelSecure) setSelectedSecurityLevel(SecurityLevelSecure)
SetSelectedSecurityLevel(SecurityLevelSecure) if FmtActiveSecurityLevel() != "Secure" {
if FmtCurrentSecurityLevel() != "Secure" { t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
t.Error("unexpected string representation")
} }
SetCurrentSecurityLevel(SecurityLevelFortress) setSelectedSecurityLevel(SecurityLevelSecure)
SetSelectedSecurityLevel(SecurityLevelFortress) AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelFortress})
if FmtCurrentSecurityLevel() != "Fortress" { if FmtActiveSecurityLevel() != "Secure*" {
t.Error("unexpected string representation") t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
} }
SetSelectedSecurityLevel(SecurityLevelDynamic) setSelectedSecurityLevel(SecurityLevelFortress)
if FmtCurrentSecurityLevel() != "Fortress*" { if FmtActiveSecurityLevel() != "Fortress" {
t.Error("unexpected string representation") t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel())
} }
} }

72
status/threat.go Normal file
View file

@ -0,0 +1,72 @@
package status
import (
"strings"
"sync"
)
// Threat describes a detected threat.
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
}
// 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()
go status.Save()
}
// DeleteThreat deletes a threat from the system status.
func DeleteThreat(id string) {
status.Lock()
defer status.Unlock()
delete(status.Threats, id)
status.updateThreatMitigationLevel()
status.autopilot()
go status.Save()
}
// 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()
var exportedThreats []*Threat
for id, threat := range status.Threats {
if strings.HasPrefix(id, idPrefix) {
exportedThreats = append(exportedThreats, threat)
}
}
return exportedThreats, &status.Mutex
}
func (s *SystemStatus) updateThreatMitigationLevel() {
// get highest mitigationLevel
var mitigationLevel uint8
for _, threat := range s.Threats {
switch threat.MitigationLevel {
case SecurityLevelDynamic, SecurityLevelSecure, SecurityLevelFortress:
if threat.MitigationLevel > mitigationLevel {
mitigationLevel = threat.MitigationLevel
}
}
}
// set new ThreatMitigationLevel
s.ThreatMitigationLevel = mitigationLevel
}

18
status/updates.go Normal file
View file

@ -0,0 +1,18 @@
package status
// Update status options
const (
UpdateStatusCurrentStable = "stable"
UpdateStatusCurrentBeta = "beta"
UpdateStatusAvailable = "available" // restart or reboot required
UpdateStatusFailed = "failed" // check logs
)
// SetUpdateStatus updates the system status with a new update status.
func SetUpdateStatus(newStatus string) {
status.Lock()
status.UpdateStatus = newStatus
status.Unlock()
go status.Save()
}