Finalize profile merging, add profile metadata state handling, re-attribute connections after profile deletion

This commit is contained in:
Daniel 2023-09-27 14:23:02 +02:00
parent 32342ec91a
commit bed5c72a6b
14 changed files with 622 additions and 96 deletions

View file

@ -2,13 +2,18 @@ package firewall
import (
"context"
"fmt"
"strings"
"github.com/safing/portbase/config"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portbase/modules/subsystems"
_ "github.com/safing/portmaster/core"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/profile"
"github.com/safing/spn/access"
"github.com/safing/spn/captain"
)
var module *modules.Module
@ -25,12 +30,6 @@ func init() {
)
}
const (
configChangeEvent = "config change"
profileConfigChangeEvent = "profile config change"
onSPNConnectEvent = "spn connect"
)
func prep() error {
network.SetDefaultFirewallHandler(verdictHandler)
@ -38,8 +37,8 @@ func prep() error {
// this will be triggered on spn enable/disable
err := module.RegisterEventHook(
"config",
configChangeEvent,
"reset connection verdicts",
config.ChangeEvent,
"reset connection verdicts after global config change",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
return nil
@ -52,10 +51,20 @@ func prep() error {
// Reset connections every time profile changes
err = module.RegisterEventHook(
"profiles",
profileConfigChangeEvent,
"reset connection verdicts",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
profile.ConfigChangeEvent,
"reset connection verdicts after profile config change",
func(ctx context.Context, eventData interface{}) error {
// Expected event data: scoped profile ID.
profileID, ok := eventData.(string)
if !ok {
return fmt.Errorf("event data is not a string: %v", eventData)
}
profileSource, profileID, ok := strings.Cut(profileID, "/")
if !ok {
return fmt.Errorf("event data does not seem to be a scoped profile ID: %v", eventData)
}
resetProfileConnectionVerdict(profileSource, profileID)
return nil
},
)
@ -67,8 +76,8 @@ func prep() error {
// connect and disconnecting is triggered on config change event but connecting takеs more time
err = module.RegisterEventHook(
"captain",
onSPNConnectEvent,
"reset connection verdicts",
captain.SPNConnectedEvent,
"reset connection verdicts on SPN connect",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
return nil
@ -83,7 +92,7 @@ func prep() error {
err = module.RegisterEventHook(
"access",
access.AccountUpdateEvent,
"update connection feature flags",
"update connection feature flags after account update",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
return nil
@ -93,6 +102,24 @@ func prep() error {
log.Errorf("filter: failed to register event hook: %s", err)
}
err = module.RegisterEventHook(
"network",
network.ConnectionReattributedEvent,
"reset verdict of re-attributed connection",
func(ctx context.Context, eventData interface{}) error {
// Expected event data: connection ID.
connID, ok := eventData.(string)
if !ok {
return fmt.Errorf("event data is not a string: %v", eventData)
}
resetSingleConnectionVerdict(connID)
return nil
},
)
if err != nil {
log.Errorf("filter: failed to register event hook: %s", err)
}
if err := registerConfig(); err != nil {
return err
}

View file

@ -43,13 +43,55 @@ var (
ownPID = os.Getpid()
)
func resetAllConnectionVerdicts() {
// Resetting will force all the connection to be evaluated by the firewall again
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
log.Info("interception: re-evaluating all connections")
func resetSingleConnectionVerdict(connID string) {
// Create tracing context.
ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
conn, ok := network.GetConnection(connID)
if !ok {
conn, ok = network.GetDNSConnection(connID)
if !ok {
tracer.Debugf("filter: could not find re-attributed connection %s for re-evaluation", connID)
return
}
}
resetConnectionVerdict(ctx, conn)
}
func resetProfileConnectionVerdict(profileSource, profileID string) {
// Create tracing context.
ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
// Resetting will force all the connection to be evaluated by the firewall again
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
tracer.Infof("filter: re-evaluating connections of %s/%s", profileSource, profileID)
// Re-evaluate all connections.
var changedVerdicts int
for _, conn := range network.GetAllConnections() {
// Check if connection is complete and attributed to the deleted profile.
if conn.DataIsComplete() &&
conn.ProcessContext.Profile == profileID &&
conn.ProcessContext.Source == profileSource {
if resetConnectionVerdict(ctx, conn) {
changedVerdicts++
}
}
}
tracer.Infof("filter: changed verdict on %d connections", changedVerdicts)
}
func resetAllConnectionVerdicts() {
// Create tracing context.
ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
// Resetting will force all the connection to be evaluated by the firewall again
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
tracer.Info("filter: re-evaluating all connections")
// Re-evaluate all connections.
var changedVerdicts int
@ -59,54 +101,60 @@ func resetAllConnectionVerdicts() {
continue
}
func() {
conn.Lock()
defer conn.Unlock()
// Update feature flags.
if err := conn.UpdateFeatures(); err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
tracer.Warningf("network: failed to update connection feature flags: %s", err)
}
// Skip internal connections:
// - Pre-authenticated connections from Portmaster
// - Redirected DNS requests
// - SPN Uplink to Home Hub
if conn.Internal {
tracer.Tracef("filter: skipping internal connection %s", conn)
return
}
tracer.Debugf("filter: re-evaluating verdict of %s", conn)
previousVerdict := conn.Verdict.Firewall
// Apply privacy filter and check tunneling.
FilterConnection(ctx, conn, nil, true, true)
// Stop existing SPN tunnel if not needed anymore.
if conn.Verdict.Active != network.VerdictRerouteToTunnel && conn.TunnelContext != nil {
err := conn.TunnelContext.StopTunnel()
if err != nil {
tracer.Debugf("filter: failed to stopped unneeded tunnel: %s", err)
}
}
// Save if verdict changed.
if conn.Verdict.Firewall != previousVerdict {
err := interception.UpdateVerdictOfConnection(conn)
if err != nil {
log.Debugf("filter: failed to update connection verdict: %s", err)
}
conn.Save()
tracer.Infof("filter: verdict of connection %s changed from %s to %s", conn, previousVerdict.Verb(), conn.VerdictVerb())
changedVerdicts++
} else {
tracer.Tracef("filter: verdict to connection %s unchanged at %s", conn, conn.VerdictVerb())
}
}()
if resetConnectionVerdict(ctx, conn) {
changedVerdicts++
}
}
tracer.Infof("filter: changed verdict on %d connections", changedVerdicts)
tracer.Submit()
}
func resetConnectionVerdict(ctx context.Context, conn *network.Connection) (verdictChanged bool) {
tracer := log.Tracer(ctx)
conn.Lock()
defer conn.Unlock()
// Update feature flags.
if err := conn.UpdateFeatures(); err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
tracer.Warningf("filter: failed to update connection feature flags: %s", err)
}
// Skip internal connections:
// - Pre-authenticated connections from Portmaster
// - Redirected DNS requests
// - SPN Uplink to Home Hub
if conn.Internal {
// tracer.Tracef("filter: skipping internal connection %s", conn)
return false
}
tracer.Debugf("filter: re-evaluating verdict of %s", conn)
previousVerdict := conn.Verdict.Firewall
// Apply privacy filter and check tunneling.
FilterConnection(ctx, conn, nil, true, true)
// Stop existing SPN tunnel if not needed anymore.
if conn.Verdict.Active != network.VerdictRerouteToTunnel && conn.TunnelContext != nil {
err := conn.TunnelContext.StopTunnel()
if err != nil {
tracer.Debugf("filter: failed to stopped unneeded tunnel: %s", err)
}
}
// Save if verdict changed.
if conn.Verdict.Firewall != previousVerdict {
err := interception.UpdateVerdictOfConnection(conn)
if err != nil {
log.Debugf("filter: failed to update connection verdict: %s", err)
}
conn.Save()
tracer.Infof("filter: verdict of connection %s changed from %s to %s", conn, previousVerdict.Verb(), conn.VerdictVerb())
return true
}
tracer.Tracef("filter: verdict to connection %s unchanged at %s", conn, conn.VerdictVerb())
return false
}
// SetNameserverIPMatcher sets a function that is used to match the internal

View file

@ -582,6 +582,11 @@ func GetAllConnections() []*Connection {
return conns.list()
}
// GetDNSConnection fetches a DNS Connection from the database.
func GetDNSConnection(dnsConnID string) (*Connection, bool) {
return dnsConns.get(dnsConnID)
}
// SetLocalIP sets the local IP address together with its network scope. The
// connection is not locked for this.
func (conn *Connection) SetLocalIP(ip net.IP) {

View file

@ -1,9 +1,16 @@
package network
import (
"context"
"fmt"
"strings"
"sync"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/network/state"
"github.com/safing/portmaster/profile"
)
var (
@ -12,8 +19,14 @@ var (
defaultFirewallHandler FirewallHandler
)
// Events.
var (
ConnectionReattributedEvent = "connection re-attributed"
)
func init() {
module = modules.Register("network", prep, start, nil, "base", "netenv", "processes")
module.RegisterEvent(ConnectionReattributedEvent, false)
}
// SetDefaultFirewallHandler sets the default firewall handler.
@ -45,5 +58,95 @@ func start() error {
module.StartServiceWorker("clean connections", 0, connectionCleaner)
module.StartServiceWorker("write open dns requests", 0, openDNSRequestWriter)
if err := module.RegisterEventHook(
"profiles",
profile.DeletedEvent,
"re-attribute connections from deleted profile",
reAttributeConnections,
); err != nil {
return err
}
return nil
}
var reAttributionLock sync.Mutex
// reAttributeConnections finds all connections of a deleted profile and re-attributes them.
// Expected event data: scoped profile ID.
func reAttributeConnections(_ context.Context, eventData any) error {
profileID, ok := eventData.(string)
if !ok {
return fmt.Errorf("event data is not a string: %v", eventData)
}
profileSource, profileID, ok := strings.Cut(profileID, "/")
if !ok {
return fmt.Errorf("event data does not seem to be a scoped profile ID: %v", eventData)
}
// Hold a lock for re-attribution, to prevent simultaneous processing of the
// same connections and make logging cleaner.
reAttributionLock.Lock()
defer reAttributionLock.Unlock()
// Create tracing context.
ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
tracer.Infof("network: re-attributing connections from deleted profile %s/%s", profileSource, profileID)
// Count and log how many connections were re-attributed.
var reAttributed int
// Re-attribute connections.
for _, conn := range conns.clone() {
// Check if connection is complete and attributed to the deleted profile.
if conn.DataIsComplete() &&
conn.ProcessContext.Profile == profileID &&
conn.ProcessContext.Source == profileSource {
reAttributeConnection(ctx, conn)
reAttributed++
tracer.Debugf("filter: re-attributed %s to %s", conn, conn.process.PrimaryProfileID)
}
}
// Re-attribute dns connections.
for _, conn := range dnsConns.clone() {
// Check if connection is complete and attributed to the deleted profile.
if conn.DataIsComplete() &&
conn.ProcessContext.Profile == profileID &&
conn.ProcessContext.Source == profileSource {
reAttributeConnection(ctx, conn)
reAttributed++
tracer.Debugf("filter: re-attributed %s to %s", conn, conn.process.PrimaryProfileID)
}
}
tracer.Infof("filter: re-attributed %d connections", reAttributed)
return nil
}
func reAttributeConnection(ctx context.Context, conn *Connection) {
// Check if data is complete.
if !conn.DataIsComplete() {
return
}
conn.Lock()
defer conn.Unlock()
// Attempt to assign new profile.
err := conn.process.RefetchProfile(ctx)
if err != nil {
log.Warningf("network: failed to refetch profile for %s: %s", conn, err)
return
}
// Set the new process context.
conn.ProcessContext = getProcessContext(ctx, conn.process)
conn.Save()
// Trigger event for re-attribution.
module.TriggerEvent(ConnectionReattributedEvent, conn.ID)
}

View file

@ -40,6 +40,24 @@ func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
return true, nil
}
// RefetchProfile removes the profile and finds and assigns a new profile.
func (p *Process) RefetchProfile(ctx context.Context) error {
p.Lock()
defer p.Unlock()
// Get special or regular profile.
localProfile, err := profile.GetLocalProfile(p.getSpecialProfileID(), p.MatchingData(), p.CreateProfileCallback)
if err != nil {
return fmt.Errorf("failed to find profile: %w", err)
}
// Assign profile to process.
p.PrimaryProfileID = localProfile.ScopedID()
p.profile = localProfile.LayeredProfile()
return nil
}
// getSpecialProfileID returns the special profile ID for the process, if any.
func (p *Process) getSpecialProfileID() (specialProfileID string) {
// Check if we need a special profile.

66
profile/api.go Normal file
View file

@ -0,0 +1,66 @@
package profile
import (
"fmt"
"github.com/safing/portbase/api"
"github.com/safing/portbase/formats/dsd"
)
func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Merge profiles",
Description: "Merge multiple profiles into a new one.",
Path: "profile/merge",
Write: api.PermitUser,
BelongsTo: module,
StructFunc: handleMergeProfiles,
}); err != nil {
return err
}
return nil
}
type mergeProfilesRequest struct {
Name string `json:"name"` // Name of the new merged profile.
To string `json:"to"` // Profile scoped ID.
From []string `json:"from"` // Profile scoped IDs.
}
type mergeprofilesResponse struct {
New string `json:"new"` // Profile scoped ID.
}
func handleMergeProfiles(ar *api.Request) (i interface{}, err error) {
request := &mergeProfilesRequest{}
_, err = dsd.MimeLoad(ar.InputData, ar.Header.Get("Content-Type"), request)
if err != nil {
return nil, fmt.Errorf("failed to parse request: %w", err)
}
// Get all profiles.
var (
primary *Profile
secondaries = make([]*Profile, 0, len(request.From))
)
if primary, err = getProfile(request.To); err != nil {
return nil, fmt.Errorf("failed to get profile %s: %w", request.To, err)
}
for _, from := range request.From {
sp, err := getProfile(from)
if err != nil {
return nil, fmt.Errorf("failed to get profile %s: %w", request.To, err)
}
secondaries = append(secondaries, sp)
}
newProfile, err := MergeProfiles(request.Name, primary, secondaries...)
if err != nil {
return nil, fmt.Errorf("failed to merge profiles: %w", err)
}
return &mergeprofilesResponse{
New: newProfile.ScopedID(),
}, nil
}

View file

@ -16,6 +16,7 @@ import (
// core:profiles/<scope>/<id>
// cache:profiles/index/<identifier>/<value>
// ProfilesDBPath is the base database path for profiles.
const ProfilesDBPath = "core:profiles/"
var profileDB = database.NewInterface(&database.Options{
@ -59,8 +60,14 @@ func startProfileUpdateChecker() error {
}
// Get active profile.
activeProfile := getActiveProfile(strings.TrimPrefix(r.Key(), ProfilesDBPath))
scopedID := strings.TrimPrefix(r.Key(), ProfilesDBPath)
activeProfile := getActiveProfile(scopedID)
if activeProfile == nil {
// Check if profile is being deleted.
if r.Meta().IsDeleted() {
meta.MarkDeleted(scopedID)
}
// Don't do any additional actions if the profile is not active.
continue profileFeed
}
@ -74,7 +81,9 @@ func startProfileUpdateChecker() error {
// Always mark as outdated if the record is being deleted.
if r.Meta().IsDeleted() {
activeProfile.outdated.Set()
module.TriggerEvent(profileConfigChange, nil)
meta.MarkDeleted(scopedID)
module.TriggerEvent(DeletedEvent, scopedID)
continue
}
@ -83,7 +92,7 @@ func startProfileUpdateChecker() error {
receivedProfile, err := EnsureProfile(r)
if err != nil || !receivedProfile.savedInternally {
activeProfile.outdated.Set()
module.TriggerEvent(profileConfigChange, nil)
module.TriggerEvent(ConfigChangeEvent, scopedID)
}
case <-ctx.Done():
return nil
@ -105,6 +114,11 @@ func (h *databaseHook) UsesPrePut() bool {
// PrePut implements the Hook interface.
func (h *databaseHook) PrePut(r record.Record) (record.Record, error) {
// Do not intervene with metadata key.
if r.Key() == profilesMetadataKey {
return r, nil
}
// convert
profile, err := EnsureProfile(r)
if err != nil {

View file

@ -202,7 +202,8 @@ func invalidDefinitionError(fields []string, msg string) error {
return fmt.Errorf(`invalid endpoint definition: "%s" - %s`, strings.Join(fields, " "), msg)
}
func parseEndpoint(value string) (endpoint Endpoint, err error) { //nolint:gocognit
//nolint:gocognit,nakedret
func parseEndpoint(value string) (endpoint Endpoint, err error) {
fields := strings.Fields(value)
if len(fields) < 2 {
return nil, fmt.Errorf(`invalid endpoint definition: "%s"`, value)

View file

@ -287,6 +287,9 @@ func loadProfile(r record.Record) (*Profile, error) {
// Set saved internally to suppress outdating profiles if saving internally.
profile.savedInternally = true
// Mark as recently seen.
meta.UpdateLastSeen(profile.ScopedID())
// return parsed profile
return profile, nil
}

View file

@ -1,8 +1,10 @@
package profile
import (
"errors"
"fmt"
"sync"
"time"
"github.com/safing/portbase/database/record"
)
@ -11,19 +13,42 @@ import (
// The new profile is saved and returned.
// Only the icon and fingerprints are inherited from other profiles.
// All other information is taken only from the primary profile.
func MergeProfiles(primary *Profile, secondaries ...*Profile) (newProfile *Profile, err error) {
func MergeProfiles(name string, primary *Profile, secondaries ...*Profile) (newProfile *Profile, err error) {
if primary == nil || len(secondaries) == 0 {
return nil, errors.New("must supply both a primary and at least one secondary profile for merging")
}
// Fill info from primary profile.
nowUnix := time.Now().Unix()
newProfile = &Profile{
Base: record.Base{},
RWMutex: sync.RWMutex{},
ID: "", // Omit ID to derive it from the new fingerprints.
Source: primary.Source,
Name: primary.Name,
Name: name,
Description: primary.Description,
Homepage: primary.Homepage,
UsePresentationPath: false, // Disable presentation path.
SecurityLevel: primary.SecurityLevel,
Config: primary.Config,
Created: nowUnix,
}
// Fall back to name of primary profile, if none is set.
if newProfile.Name == "" {
newProfile.Name = primary.Name
}
// If any profile was edited, set LastEdited to now.
if primary.LastEdited > 0 {
newProfile.LastEdited = nowUnix
} else {
for _, sp := range secondaries {
if sp.LastEdited > 0 {
newProfile.LastEdited = nowUnix
break
}
}
}
// Collect all icons.
@ -35,7 +60,7 @@ func MergeProfiles(primary *Profile, secondaries ...*Profile) (newProfile *Profi
newProfile.Icons = sortAndCompactIcons(newProfile.Icons)
// Collect all fingerprints.
newProfile.Fingerprints = make([]Fingerprint, 0, len(secondaries)+1) // Guess the needed space.
newProfile.Fingerprints = make([]Fingerprint, 0, len(primary.Fingerprints)+len(secondaries)) // Guess the needed space.
newProfile.Fingerprints = addFingerprints(newProfile.Fingerprints, primary.Fingerprints, primary.ScopedID())
for _, sp := range secondaries {
newProfile.Fingerprints = addFingerprints(newProfile.Fingerprints, sp.Fingerprints, sp.ScopedID())
@ -44,26 +69,19 @@ func MergeProfiles(primary *Profile, secondaries ...*Profile) (newProfile *Profi
// Save new profile.
newProfile = New(newProfile)
err = newProfile.Save()
if err != nil {
if err := newProfile.Save(); err != nil {
return nil, fmt.Errorf("failed to save merged profile: %w", err)
}
// FIXME: Should we ... ?
// newProfile.updateMetadata()
// newProfile.updateMetadataFromSystem()
// Delete all previous profiles.
// FIXME:
/*
primary.Meta().Delete()
// Set as outdated and remove from active profiles.
// Signify that profile was deleted and save for sync.
for _, sp := range secondaries {
sp.Meta().Delete()
// Set as outdated and remove from active profiles.
// Signify that profile was deleted and save for sync.
if err := primary.delete(); err != nil {
return nil, fmt.Errorf("failed to delete primary profile %s: %w", primary.ScopedID(), err)
}
for _, sp := range secondaries {
if err := sp.delete(); err != nil {
return nil, fmt.Errorf("failed to delete secondary profile %s: %w", sp.ScopedID(), err)
}
*/
}
return newProfile, nil
}

184
profile/meta.go Normal file
View file

@ -0,0 +1,184 @@
package profile
import (
"fmt"
"sync"
"time"
"github.com/safing/portbase/database/record"
)
// ProfilesMetadata holds metadata about all profiles that are not fit to be
// stored with the profiles themselves.
type ProfilesMetadata struct {
record.Base
sync.Mutex
States map[string]*MetaState
}
// MetaState describes the state of a profile.
type MetaState struct {
State string
At time.Time
}
// Profile metadata states.
const (
MetaStateSeen = "seen"
MetaStateDeleted = "deleted"
)
// EnsureProfilesMetadata ensures that the given record is a *ProfilesMetadata, and returns it.
func EnsureProfilesMetadata(r record.Record) (*ProfilesMetadata, error) {
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
newMeta := &ProfilesMetadata{}
err := record.Unwrap(r, newMeta)
if err != nil {
return nil, err
}
return newMeta, nil
}
// or adjust type
newMeta, ok := r.(*ProfilesMetadata)
if !ok {
return nil, fmt.Errorf("record not of type *Profile, but %T", r)
}
return newMeta, nil
}
var (
profilesMetadataKey = ProfilesDBPath + "meta"
meta *ProfilesMetadata
removeDeletedEntriesAfter = 30 * 24 * time.Hour
)
// loadProfilesMetadata loads the profile metadata from the database.
// It may only be called during module starting, as there is no lock for "meta" itself.
func loadProfilesMetadata() error {
r, err := profileDB.Get(profilesMetadataKey)
if err != nil {
return err
}
loadedMeta, err := EnsureProfilesMetadata(r)
if err != nil {
return err
}
// Set package variable.
meta = loadedMeta
return nil
}
func (meta *ProfilesMetadata) check() {
if meta.States == nil {
meta.States = make(map[string]*MetaState)
}
}
// Save saves the profile metadata to the database.
func (meta *ProfilesMetadata) Save() error {
if meta == nil {
return nil
}
func() {
meta.Lock()
defer meta.Unlock()
if !meta.KeyIsSet() {
meta.SetKey(profilesMetadataKey)
}
}()
meta.Clean()
return profileDB.Put(meta)
}
// Clean removes old entries.
func (meta *ProfilesMetadata) Clean() {
if meta == nil {
return
}
meta.Lock()
defer meta.Unlock()
for key, state := range meta.States {
switch {
case state == nil:
delete(meta.States, key)
case state.State != MetaStateDeleted:
continue
case time.Since(state.At) > removeDeletedEntriesAfter:
delete(meta.States, key)
}
}
}
// GetLastSeen returns when the profile with the given ID was last seen.
func (meta *ProfilesMetadata) GetLastSeen(scopedID string) *time.Time {
if meta == nil {
return nil
}
meta.Lock()
defer meta.Unlock()
state := meta.States[scopedID]
switch {
case state == nil:
return nil
case state.State == MetaStateSeen:
return &state.At
default:
return nil
}
}
// UpdateLastSeen sets the profile with the given ID as last seen now.
func (meta *ProfilesMetadata) UpdateLastSeen(scopedID string) {
if meta == nil {
return
}
meta.Lock()
defer meta.Unlock()
meta.States[scopedID] = &MetaState{
State: MetaStateSeen,
At: time.Now().UTC(),
}
}
// MarkDeleted marks the profile with the given ID as deleted.
func (meta *ProfilesMetadata) MarkDeleted(scopedID string) {
if meta == nil {
return
}
meta.Lock()
defer meta.Unlock()
meta.States[scopedID] = &MetaState{
State: MetaStateDeleted,
At: time.Now().UTC(),
}
}
// RemoveState removes any state of the profile with the given ID.
func (meta *ProfilesMetadata) RemoveState(scopedID string) {
if meta == nil {
return
}
meta.Lock()
defer meta.Unlock()
delete(meta.States, scopedID)
}

View file

@ -32,11 +32,11 @@ func registerMigrations() error {
Version: "v1.4.7",
MigrateFunc: migrateIcons,
},
// migration.Migration{
// Description: "Migrate from random profile IDs to fingerprint-derived IDs",
// Version: "v1.5.1",
// MigrateFunc: migrateToDerivedIDs,
// },
migration.Migration{
Description: "Migrate from random profile IDs to fingerprint-derived IDs",
Version: "v1.5.0",
MigrateFunc: migrateToDerivedIDs,
},
)
}

View file

@ -1,8 +1,10 @@
package profile
import (
"errors"
"os"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/migration"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
@ -16,13 +18,16 @@ var (
updatesPath string
)
// Events.
const (
profileConfigChange = "profile config change"
ConfigChangeEvent = "profile config change"
DeletedEvent = "profile deleted"
)
func init() {
module = modules.Register("profiles", prep, start, nil, "base", "updates")
module.RegisterEvent(profileConfigChange, true)
module = modules.Register("profiles", prep, start, stop, "base", "updates")
module.RegisterEvent(ConfigChangeEvent, true)
module.RegisterEvent(DeletedEvent, true)
}
func prep() error {
@ -47,6 +52,14 @@ func start() error {
updatesPath += string(os.PathSeparator)
}
if err := loadProfilesMetadata(); err != nil {
if !errors.Is(err, database.ErrNotFound) {
log.Warningf("profile: failed to load profiles metadata, falling back to empty state: %s", err)
}
meta = &ProfilesMetadata{}
}
meta.check()
if err := migrations.Migrate(module.Ctx); err != nil {
log.Errorf("profile: migrations failed: %s", err)
}
@ -73,5 +86,13 @@ func start() error {
log.Warningf("profile: error during loading global profile from configuration: %s", err)
}
if err := registerAPIEndpoints(); err != nil {
return err
}
return nil
}
func stop() error {
return meta.Save()
}

View file

@ -304,6 +304,24 @@ func (profile *Profile) Save() error {
return profileDB.Put(profile)
}
// delete deletes the profile from the database.
func (profile *Profile) delete() error {
// Check if a key is set.
if !profile.KeyIsSet() {
return errors.New("key is not set")
}
// Delete from database.
profile.Meta().Delete()
err := profileDB.Put(profile)
if err != nil {
return err
}
// Post handling is done by the profile update feed.
return nil
}
// MarkStillActive marks the profile as still active.
func (profile *Profile) MarkStillActive() {
atomic.StoreInt64(profile.lastActive, time.Now().Unix())