Merge pull request #152 from safing/feature/status-n-notifs

Refactor status package to use portbase/runtime.
This commit is contained in:
Patrick Pacher 2020-09-22 16:54:54 +02:00 committed by GitHub
commit 51c4835954
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 583 additions and 576 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

@ -36,6 +36,7 @@ func registerConfig() error {
DefaultValue: defaultDevMode,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: 127,
config.CategoryAnnotation: "Development",
},
})
if err != nil {
@ -52,6 +53,7 @@ func registerConfig() error {
DefaultValue: true, // TODO: turn off by default on unsupported systems
Annotations: config.Annotations{
config.DisplayOrderAnnotation: 32,
config.CategoryAnnotation: "General",
},
})
if err != nil {

View file

@ -36,6 +36,7 @@ func registerConfig() error {
DefaultValue: true,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionPermanentVerdictsOrder,
config.CategoryAnnotation: "Advanced",
},
})
if err != nil {
@ -53,6 +54,7 @@ func registerConfig() error {
DefaultValue: true,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionAskWithSystemNotificationsOrder,
config.CategoryAnnotation: "General",
},
})
if err != nil {
@ -70,6 +72,7 @@ func registerConfig() error {
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionAskTimeoutOrder,
config.UnitAnnotation: "seconds",
config.CategoryAnnotation: "General",
},
})
if err != nil {

View file

@ -31,6 +31,9 @@ func init() {
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelBeta,
DefaultValue: true,
Annotations: config.Annotations{
config.CategoryAnnotation: "General",
},
},
)
}

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

@ -22,6 +22,7 @@ func registerConfiguration() error {
DefaultValue: true,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: 144,
config.CategoryAnnotation: "Development",
},
})
if err != nil {

View file

@ -103,6 +103,7 @@ func registerConfiguration() error {
Annotations: config.Annotations{
config.DisplayHintAnnotation: config.DisplayHintOneOf,
config.DisplayOrderAnnotation: cfgOptionDefaultActionOrder,
config.CategoryAnnotation: "General",
},
PossibleValues: []config.PossibleValue{
{
@ -138,6 +139,7 @@ func registerConfiguration() error {
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionDisableAutoPermitOrder,
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.CategoryAnnotation: "Advanced",
},
PossibleValues: status.SecurityLevelValues,
})
@ -175,15 +177,16 @@ Examples:
// Endpoint Filter List
err = config.Register(&config.Option{
Name: "Endpoint Filter List",
Name: "Outgoing Rules",
Key: CfgOptionEndpointsKey,
Description: "Filter outgoing connections by matching the destination endpoint. Network Scope restrictions still apply.",
Description: "Rules that apply to outgoing network connections. Network Scope restrictions still apply.",
Help: filterListHelp,
OptType: config.OptTypeStringArray,
DefaultValue: []string{},
Annotations: config.Annotations{
config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList,
config.DisplayOrderAnnotation: cfgOptionEndpointsOrder,
config.CategoryAnnotation: "Rules",
},
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
})
@ -195,15 +198,16 @@ Examples:
// Service Endpoint Filter List
err = config.Register(&config.Option{
Name: "Service Endpoint Filter List",
Name: "Incoming Rules",
Key: CfgOptionServiceEndpointsKey,
Description: "Filter incoming connections by matching the source endpoint. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.",
Description: "Rules that apply to incoming network connections. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.",
Help: filterListHelp,
OptType: config.OptTypeStringArray,
DefaultValue: []string{"+ Localhost"},
Annotations: config.Annotations{
config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList,
config.DisplayOrderAnnotation: cfgOptionServiceEndpointsOrder,
config.CategoryAnnotation: "Rules",
},
ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`,
})
@ -223,6 +227,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: "filter list",
config.DisplayOrderAnnotation: cfgOptionFilterListsOrder,
config.CategoryAnnotation: "Rules",
},
ValidationRegex: `^[a-zA-Z0-9\-]+$`,
})
@ -243,6 +248,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionFilterCNAMEOrder,
config.CategoryAnnotation: "DNS",
},
PossibleValues: status.SecurityLevelValues,
})
@ -263,6 +269,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionFilterSubDomainsOrder,
config.CategoryAnnotation: "DNS",
},
})
if err != nil {
@ -283,6 +290,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionBlockScopeLocalOrder,
config.CategoryAnnotation: "Scopes & Types",
},
})
if err != nil {
@ -302,6 +310,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionBlockScopeLANOrder,
config.CategoryAnnotation: "Scopes & Types",
},
})
if err != nil {
@ -321,6 +330,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionBlockScopeInternetOrder,
config.CategoryAnnotation: "Scopes & Types",
},
})
if err != nil {
@ -340,6 +350,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionBlockP2POrder,
config.CategoryAnnotation: "Scopes & Types",
},
})
if err != nil {
@ -359,6 +370,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionBlockInboundOrder,
config.CategoryAnnotation: "Scopes & Types",
},
})
if err != nil {
@ -379,6 +391,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionEnforceSPNOrder,
config.CategoryAnnotation: "Advanced",
},
})
if err != nil {
@ -400,6 +413,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionRemoveOutOfScopeDNSOrder,
config.CategoryAnnotation: "DNS",
},
})
if err != nil {
@ -421,6 +435,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionRemoveBlockedDNSOrder,
config.CategoryAnnotation: "DNS",
},
})
if err != nil {
@ -441,6 +456,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionDomainHeuristicsOrder,
config.CategoryAnnotation: "DNS",
},
})
if err != nil {
@ -461,6 +477,7 @@ Examples:
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionPreventBypassingOrder,
config.CategoryAnnotation: "Advanced",
},
})
if err != nil {

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"
@ -113,6 +113,7 @@ Parameters:
ValidationRegex: fmt.Sprintf("^(%s|%s|%s)://.*", ServerTypeDoT, ServerTypeDNS, ServerTypeTCP),
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNameServersOrder,
config.CategoryAnnotation: "Servers",
},
})
if err != nil {
@ -122,6 +123,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,
@ -130,6 +132,7 @@ Parameters:
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNameserverRetryRateOrder,
config.UnitAnnotation: "seconds",
config.CategoryAnnotation: "Servers",
},
})
if err != nil {
@ -137,25 +140,6 @@ Parameters:
}
nameserverRetryRate = config.Concurrent.GetAsInt(CfgOptionNameserverRetryRateKey, 600)
err = config.Register(&config.Option{
Name: "Do not use Multicast DNS",
Key: CfgOptionNoMulticastDNSKey,
Description: "Multicast DNS queries other devices in the local network",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: status.SecurityLevelsHighAndExtreme,
PossibleValues: status.SecurityLevelValues,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNoMulticastDNSOrder,
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
},
})
if err != nil {
return err
}
noMulticastDNS = status.ConfigIsActiveConcurrent(CfgOptionNoMulticastDNSKey)
err = config.Register(&config.Option{
Name: "Do not use assigned Nameservers",
Key: CfgOptionNoAssignedNameserversKey,
@ -168,12 +152,33 @@ Parameters:
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNoAssignedNameserversOrder,
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.CategoryAnnotation: "Servers",
},
})
if err != nil {
return err
}
noAssignedNameservers = status.ConfigIsActiveConcurrent(CfgOptionNoAssignedNameserversKey)
noAssignedNameservers = status.SecurityLevelOption(CfgOptionNoAssignedNameserversKey)
err = config.Register(&config.Option{
Name: "Do not use Multicast DNS",
Key: CfgOptionNoMulticastDNSKey,
Description: "Multicast DNS queries other devices in the local network",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: status.SecurityLevelsHighAndExtreme,
PossibleValues: status.SecurityLevelValues,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNoMulticastDNSOrder,
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.CategoryAnnotation: "Resolving",
},
})
if err != nil {
return err
}
noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey)
err = config.Register(&config.Option{
Name: "Do not resolve insecurely",
@ -187,12 +192,13 @@ Parameters:
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNoInsecureProtocolsOrder,
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.CategoryAnnotation: "Resolving",
},
})
if err != nil {
return err
}
noInsecureProtocols = status.ConfigIsActiveConcurrent(CfgOptionNoInsecureProtocolsKey)
noInsecureProtocols = status.SecurityLevelOption(CfgOptionNoInsecureProtocolsKey)
err = config.Register(&config.Option{
Name: "Do not resolve special domains",
@ -206,12 +212,13 @@ Parameters:
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionDontResolveSpecialDomainsOrder,
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.CategoryAnnotation: "Resolving",
},
})
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
*notifications.Notification
}
// 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
// TODO: add locking
// Data may holds threat-specific data.
Data interface{}
}
// AddOrUpdateThreat adds or updates a new threat in the system status.
func AddOrUpdateThreat(new *Threat) {
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()
status.Threats[new.ID] = new
status.updateThreatMitigationLevel()
status.autopilot()
status.SaveAsync()
return t
}
// DeleteThreat deletes a threat from the system status.
func DeleteThreat(id string) {
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()
delete(status.Threats, id)
status.updateThreatMitigationLevel()
status.autopilot()
status.SaveAsync()
t.threatData().Data = data
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()
// SetMitigationLevel sets the mitigation level of the
// threat data.
func (t *Threat) SetMitigationLevel(lvl uint8) *Threat {
t.Lock()
defer t.Unlock()
var exportedThreats []*Threat
for id, threat := range status.Threats {
if strings.HasPrefix(id, idPrefix) {
exportedThreats = append(exportedThreats, threat)
}
}
return exportedThreats, &status.Mutex
t.threatData().MitigationLevel = lvl
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
}
}
// 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)
}
// set new ThreatMitigationLevel
s.ThreatMitigationLevel = mitigationLevel
t.Save()
return t
}
// 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)
}
payload, ok := t.EventData.(*ThreatPayload)
if !ok {
log.Warningf("unexpected type %T in thread notification payload", t.EventData)
return new(ThreatPayload)
}
return payload
}

View file

@ -44,6 +44,7 @@ func registerConfig() error {
Annotations: config.Annotations{
config.DisplayOrderAnnotation: 1,
config.DisplayHintAnnotation: config.DisplayHintOneOf,
config.CategoryAnnotation: "Expertise & Release",
},
})
if err != nil {
@ -61,6 +62,7 @@ func registerConfig() error {
DefaultValue: false,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: 64,
config.CategoryAnnotation: "General",
},
})
if err != nil {