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
new, ok := r.(*Profile)
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
}

View file

@ -31,7 +31,7 @@ func updateListener(sub *database.Subscription) {
profile, err := EnsureProfile(r)
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
}

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 {
activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic))
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 {
activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelDynamic))
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 (
currentSecurityLevel *uint32
activeSecurityLevel *uint32
selectedSecurityLevel *uint32
threatLevel *uint32
portmasterStatus *uint32
gate17Status *uint32
)
func init() {
var (
currentSecurityLevelValue uint32
activeSecurityLevelValue uint32
selectedSecurityLevelValue uint32
threatLevelValue uint32
portmasterStatusValue uint32
gate17StatusValue uint32
)
currentSecurityLevel = &currentSecurityLevelValue
activeSecurityLevel = &activeSecurityLevelValue
selectedSecurityLevel = &selectedSecurityLevelValue
threatLevel = &threatLevelValue
portmasterStatus = &portmasterStatusValue
gate17Status = &gate17StatusValue
}
// CurrentSecurityLevel returns the current security level.
func CurrentSecurityLevel() uint8 {
return uint8(atomic.LoadUint32(currentSecurityLevel))
// ActiveSecurityLevel returns the current security level.
func ActiveSecurityLevel() uint8 {
return uint8(atomic.LoadUint32(activeSecurityLevel))
}
// SelectedSecurityLevel returns the selected security level.
@ -38,11 +35,6 @@ func SelectedSecurityLevel() uint8 {
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.
func PortmasterStatus() uint8 {
return uint8(atomic.LoadUint32(portmasterStatus))

View file

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

View file

@ -1,25 +1,59 @@
package status
import (
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/log"
"github.com/Safing/portbase/modules"
)
var (
shutdownSignal = make(chan struct{})
)
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 {
log.Infof("switching to default active security level: dynamic")
SetCurrentSecurityLevel(SecurityLevelDynamic)
// 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)
loadedStatus = nil
}
case database.ErrNotFound:
// create new status
default:
log.Criticalf("status: failed to load system status: %s", err)
}
if SelectedSecurityLevel() == SecurityLevelOff {
log.Infof("switching to default selected security level: dynamic")
SetSelectedSecurityLevel(SecurityLevelDynamic)
// activate loaded status, if available
if loadedStatus != nil {
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
}

View file

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

View file

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

View file

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

View file

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