From 033dceab5b6a94802417c6f3126efe620b7f5d11 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 17 Apr 2020 21:52:06 +0200 Subject: [PATCH] Add support for unidentified/system processes/profiles --- firewall/firewall.go | 1 + firewall/master.go | 2 ++ nameserver/nameserver.go | 1 + network/connection.go | 11 +++++-- network/dns.go | 11 ++++++- process/find.go | 4 +-- process/process.go | 22 ++++++------- process/profile.go | 8 ++--- process/special.go | 67 ++++++++++++++++++++++++++++++++++++++ process/unknown.go | 26 --------------- profile/active.go | 36 +++++++++++++++++++- profile/config-update.go | 4 +-- profile/module.go | 2 ++ profile/profile-layered.go | 7 +++- profile/profile.go | 54 +++++++++++++++++++++--------- profile/special.go | 54 ++++++++++++++++++++++++++++++ 16 files changed, 243 insertions(+), 67 deletions(-) create mode 100644 process/special.go delete mode 100644 process/unknown.go create mode 100644 profile/special.go diff --git a/firewall/firewall.go b/firewall/firewall.go index 530f7437..5ccc8677 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -233,6 +233,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { if ps.isMe { // approve conn.Accept("internally approved") + conn.Hidden = true // finish conn.StopFirewallHandler() issueVerdict(conn, pkt, 0, true) diff --git a/firewall/master.go b/firewall/master.go index 9650b625..9cb55bc1 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -50,6 +50,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: if conn.Process().Pid == os.Getpid() { log.Infof("filter: granting own connection %s", conn) conn.Verdict = network.VerdictAccept + conn.Hidden = true return } @@ -75,6 +76,7 @@ func DecideOnConnection(conn *network.Connection, pkt packet.Packet) { //nolint: log.Warningf("filter: failed to find load local peer process with PID %d: %s", otherPid, err) } else if otherProcess.Pid == conn.Process().Pid { conn.Accept("connection to self") + conn.Hidden = true return } } diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 7eee0c4d..8f2dbd65 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -199,6 +199,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, query *dns.Msg) er } }() + // TODO: this has been obsoleted due to special profiles if conn.Process().Profile() == nil { tracer.Infof("nameserver: failed to find process for request %s, returning NXDOMAIN", conn) returnNXDomain(w, query) diff --git a/network/connection.go b/network/connection.go index d2ada322..3b2c6a84 100644 --- a/network/connection.go +++ b/network/connection.go @@ -41,6 +41,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment VerdictPermanent bool Inspecting bool Encrypted bool // TODO + Hidden bool pktQueue chan packet.Packet firewallHandler FirewallHandler @@ -58,7 +59,7 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, ip net.IP, po proc, err := process.GetProcessByEndpoints(ctx, ip, port, dnsAddress, dnsPort, packet.UDP) if err != nil { log.Warningf("network: failed to find process of dns request for %s: %s", fqdn, err) - proc = process.UnknownProcess + proc = process.GetUnidentifiedProcess(ctx) } timestamp := time.Now().Unix() @@ -80,7 +81,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { proc, inbound, err := process.GetProcessByPacket(pkt) if err != nil { log.Warningf("network: failed to find process of packet %s: %s", pkt, err) - proc = process.UnknownProcess + proc = process.GetUnidentifiedProcess(pkt.Ctx()) } var scope string @@ -270,7 +271,11 @@ func (conn *Connection) Save() { // delete deletes a link from the storage and propagates the change. Nothing is locked - both the conns map and the connection itself require locking func (conn *Connection) delete() { - delete(conns, conn.ID) + if conn.ID == "" { + delete(dnsConns, strconv.Itoa(conn.process.Pid)+"/"+conn.Scope) + } else { + delete(conns, conn.ID) + } conn.Meta().Delete() dbController.PushUpdate(conn) diff --git a/network/dns.go b/network/dns.go index 88ca7be3..43c0efcf 100644 --- a/network/dns.go +++ b/network/dns.go @@ -23,7 +23,16 @@ func removeOpenDNSRequest(pid int, fqdn string) { defer openDNSRequestsLock.Unlock() key := strconv.Itoa(pid) + "/" + fqdn - delete(openDNSRequests, key) + _, ok := openDNSRequests[key] + if ok { + delete(openDNSRequests, key) + return + } + + // check if there is an open dns request from an unidentified process + if pid >= 0 { + delete(openDNSRequests, "-1/"+fqdn) + } } // SaveOpenDNSRequest saves a dns request connection that was allowed to proceed. diff --git a/process/find.go b/process/find.go index 997d487b..0e609f52 100644 --- a/process/find.go +++ b/process/find.go @@ -58,7 +58,7 @@ func GetPidByPacket(pkt packet.Packet) (pid int, direction bool, err error) { func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, err error) { if !enableProcessDetection() { log.Tracer(pkt.Ctx()).Tracef("process: process detection disabled") - return UnknownProcess, direction, nil + return GetUnidentifiedProcess(pkt.Ctx()), pkt.Info().Direction, nil } log.Tracer(pkt.Ctx()).Tracef("process: getting process and profile by packet") @@ -116,7 +116,7 @@ func GetPidByEndpoints(localIP net.IP, localPort uint16, remoteIP net.IP, remote func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16, remoteIP net.IP, remotePort uint16, protocol packet.IPProtocol) (process *Process, err error) { if !enableProcessDetection() { log.Tracer(ctx).Tracef("process: process detection disabled") - return UnknownProcess, nil + return GetUnidentifiedProcess(ctx), nil } log.Tracer(ctx).Tracef("process: getting process and profile by endpoints") diff --git a/process/process.go b/process/process.go index 2ab29383..acdadeb8 100644 --- a/process/process.go +++ b/process/process.go @@ -75,11 +75,11 @@ func (p *Process) String() string { func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting primary process for PID %d", pid) - if pid == -1 { - return UnknownProcess, nil + if pid <= -1 { + return GetUnidentifiedProcess(ctx), nil } if pid == 0 { - return OSProcess, nil + return GetSystemProcess(ctx), nil } process, err := loadProcess(ctx, pid) @@ -88,8 +88,8 @@ func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { } for { - if process.ParentPid == 0 { - return OSProcess, nil + if process.ParentPid <= 0 { + return process, nil } parentProcess, err := loadProcess(ctx, process.ParentPid) if err != nil { @@ -121,11 +121,11 @@ func GetOrFindPrimaryProcess(ctx context.Context, pid int) (*Process, error) { func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) { log.Tracer(ctx).Tracef("process: getting process for PID %d", pid) - if pid == -1 { - return UnknownProcess, nil + if pid <= -1 { + return GetUnidentifiedProcess(ctx), nil } if pid == 0 { - return OSProcess, nil + return GetSystemProcess(ctx), nil } p, err := loadProcess(ctx, pid) @@ -184,11 +184,11 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) { } func loadProcess(ctx context.Context, pid int) (*Process, error) { - if pid == -1 { - return UnknownProcess, nil + if pid <= -1 { + return GetUnidentifiedProcess(ctx), nil } if pid == 0 { - return OSProcess, nil + return GetSystemProcess(ctx), nil } process, ok := GetProcessFromStorage(pid) diff --git a/process/profile.go b/process/profile.go index 388684fa..0f0ad5c6 100644 --- a/process/profile.go +++ b/process/profile.go @@ -15,6 +15,8 @@ func (p *Process) GetProfile(ctx context.Context) error { // only find profiles if not already done. if p.profile != nil { log.Tracer(ctx).Trace("process: profile already loaded") + // mark profile as used + p.profile.MarkUsed() return nil } log.Tracer(ctx).Trace("process: loading profile") @@ -29,10 +31,8 @@ func (p *Process) GetProfile(ctx context.Context) error { localProfile.Name = p.ExecName } - // mark as used and save - if localProfile.MarkUsed() { - _ = localProfile.Save() - } + // mark profile as used + localProfile.MarkUsed() p.LocalProfileKey = localProfile.Key() p.profile = profile.NewLayeredProfile(localProfile) diff --git a/process/special.go b/process/special.go new file mode 100644 index 00000000..8cbf6130 --- /dev/null +++ b/process/special.go @@ -0,0 +1,67 @@ +package process + +import ( + "context" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/profile" +) + +var ( + // unidentifiedProcess is used when a process cannot be found. + unidentifiedProcess = &Process{ + UserID: -1, + UserName: "Unknown", + Pid: -1, + ParentPid: -1, + Name: "Unidentified Processes", + } + + // systemProcess is used to represent the Kernel. + systemProcess = &Process{ + UserID: 0, + UserName: "Kernel", + Pid: 0, + ParentPid: 0, + Name: "Operating System", + } +) + +func GetUnidentifiedProcess(ctx context.Context) *Process { + return getSpecialProcess(ctx, unidentifiedProcess, profile.GetUnidentifiedProfile) +} + +func GetSystemProcess(ctx context.Context) *Process { + return getSpecialProcess(ctx, systemProcess, profile.GetSystemProfile) +} + +func getSpecialProcess(ctx context.Context, p *Process, getProfile func() *profile.Profile) *Process { + p.Lock() + defer p.Unlock() + + if p.FirstSeen == 0 { + p.FirstSeen = time.Now().Unix() + } + + // only find profiles if not already done. + if p.profile != nil { + log.Tracer(ctx).Trace("process: special profile already loaded") + // mark profile as used + p.profile.MarkUsed() + return p + } + log.Tracer(ctx).Trace("process: loading special profile") + + // get profile + localProfile := getProfile() + + // mark profile as used + localProfile.MarkUsed() + + p.LocalProfileKey = localProfile.Key() + p.profile = profile.NewLayeredProfile(localProfile) + + go p.Save() + return p +} diff --git a/process/unknown.go b/process/unknown.go deleted file mode 100644 index 4f6cde12..00000000 --- a/process/unknown.go +++ /dev/null @@ -1,26 +0,0 @@ -package process - -var ( - // UnknownProcess is used when a process cannot be found. - UnknownProcess = &Process{ - UserID: -1, - UserName: "Unknown", - Pid: -1, - ParentPid: -1, - Name: "Unknown Processes", - } - - // OSProcess is used to represent the Kernel. - OSProcess = &Process{ - UserID: 0, - UserName: "Kernel", - Pid: 0, - ParentPid: 0, - Name: "Operating System", - } -) - -func init() { - UnknownProcess.Save() - OSProcess.Save() -} diff --git a/profile/active.go b/profile/active.go index ff8f3d3e..3d45a727 100644 --- a/profile/active.go +++ b/profile/active.go @@ -1,7 +1,14 @@ package profile import ( + "context" "sync" + "time" +) + +const ( + activeProfileCleanerTickDuration = 10 * time.Minute + activeProfileCleanerThreshold = 1 * time.Hour ) var ( @@ -38,7 +45,34 @@ func markActiveProfileAsOutdated(scopedID string) { profile, ok := activeProfiles[scopedID] if ok { - profile.oudated.Set() + profile.outdated.Set() delete(activeProfiles, scopedID) } } + +func cleanActiveProfiles(ctx context.Context) error { //nolint:param // need to conform to interface + for { + select { + case <-time.After(activeProfileCleanerTickDuration): + + threshold := time.Now().Add(-activeProfileCleanerThreshold) + + activeProfilesLock.Lock() + for id, profile := range activeProfiles { + // get last used + profile.Lock() + lastUsed := profile.lastUsed + profile.Unlock() + // remove if not used for a while + if lastUsed.Before(threshold) { + profile.outdated.Set() + delete(activeProfiles, id) + } + } + activeProfilesLock.Unlock() + + case <-ctx.Done(): + return nil + } + } +} diff --git a/profile/config-update.go b/profile/config-update.go index 15e62b8e..b4c3f5e4 100644 --- a/profile/config-update.go +++ b/profile/config-update.go @@ -70,8 +70,8 @@ func updateGlobalConfigProfile(ctx context.Context, data interface{}) error { // build global profile for reference profile := &Profile{ - ID: "config", - Source: SourceGlobal, + ID: "global-config", + Source: SourceSpecial, Name: "Global Configuration", Config: make(map[string]interface{}), internalSave: true, diff --git a/profile/module.go b/profile/module.go index 615eddcf..8d962a8a 100644 --- a/profile/module.go +++ b/profile/module.go @@ -42,6 +42,8 @@ func start() error { return err } + module.StartServiceWorker("clean active profiles", 0, cleanActiveProfiles) + err = updateGlobalConfigProfile(module.Ctx, nil) if err != nil { log.Warningf("profile: error during loading global profile from configuration: %s", err) diff --git a/profile/profile-layered.go b/profile/profile-layered.go index 90f0478b..6bf3ea2e 100644 --- a/profile/profile-layered.go +++ b/profile/profile-layered.go @@ -123,7 +123,7 @@ func (lp *LayeredProfile) Update() (revisionCounter uint64) { var changed bool for i, layer := range lp.layers { - if layer.oudated.IsSet() { + if layer.outdated.IsSet() { changed = true // update layer newLayer, err := GetProfile(layer.Source, layer.ID) @@ -170,6 +170,11 @@ func (lp *LayeredProfile) updateCaches() { // TODO: ignore community profiles } +// MarkUsed marks the localProfile as used. +func (lp *LayeredProfile) MarkUsed() { + lp.localProfile.MarkUsed() +} + // SecurityLevel returns the highest security level of all layered profiles. func (lp *LayeredProfile) SecurityLevel() uint8 { return uint8(atomic.LoadUint32(lp.securityLevel)) diff --git a/profile/profile.go b/profile/profile.go index 46e3b447..70f7c162 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -19,15 +19,15 @@ import ( ) var ( - lastUsedUpdateThreshold = 1 * time.Hour + lastUsedUpdateThreshold = 24 * time.Hour ) // Profile Sources const ( - SourceLocal string = "local" + SourceLocal string = "local" // local, editable + SourceSpecial string = "special" // specials (read-only) SourceCommunity string = "community" SourceEnterprise string = "enterprise" - SourceGlobal string = "global" ) // Default Action IDs @@ -77,7 +77,8 @@ type Profile struct { //nolint:maligned // not worth the effort filterListIDs []string // Lifecycle Management - oudated *abool.AtomicBool + outdated *abool.AtomicBool + lastUsed time.Time // Framework // If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process/actor must be found @@ -94,7 +95,7 @@ type Profile struct { //nolint:maligned // not worth the effort func (profile *Profile) prepConfig() (err error) { // prepare configuration profile.configPerspective, err = config.NewPerspective(profile.Config) - profile.oudated = abool.New() + profile.outdated = abool.New() return } @@ -156,10 +157,11 @@ func (profile *Profile) parseConfig() error { // New returns a new Profile. func New() *Profile { profile := &Profile{ - ID: uuid.NewV4().String(), - Source: SourceLocal, - Created: time.Now().Unix(), - Config: make(map[string]interface{}), + ID: uuid.NewV4().String(), + Source: SourceLocal, + Created: time.Now().Unix(), + Config: make(map[string]interface{}), + internalSave: true, } // create placeholders @@ -190,13 +192,26 @@ func (profile *Profile) Save() error { return profileDB.Put(profile) } -// MarkUsed marks the profile as used, eventually. -func (profile *Profile) MarkUsed() (updated bool) { +// MarkUsed marks the profile as used and saves it when it has changed. +func (profile *Profile) MarkUsed() { + profile.Lock() + // lastUsed + profile.lastUsed = time.Now() + + // ApproxLastUsed + save := false if time.Now().Add(-lastUsedUpdateThreshold).Unix() > profile.ApproxLastUsed { profile.ApproxLastUsed = time.Now().Unix() - return true + save = true + } + profile.Unlock() + + if save { + err := profile.Save() + if err != nil { + log.Warningf("profiles: failed to save profile %s after marking as used: %s", profile.ScopedID(), err) + } } - return false } // String returns a string representation of the Profile. @@ -224,8 +239,6 @@ func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) { endpointList = append(endpointList, newEntry) profile.Config[cfgKey] = endpointList - // save without full reload - profile.internalSave = true profile.Unlock() err := profile.Save() if err != nil { @@ -233,10 +246,13 @@ func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) { } // reload manually + profile.Lock() + profile.dataParsed = false err = profile.parseConfig() if err != nil { log.Warningf("profile: failed to parse profile config after adding endpoint: %s", err) } + profile.Unlock() } // GetProfile loads a profile from the database. @@ -249,6 +265,7 @@ func GetProfileByScopedID(scopedID string) (*Profile, error) { // check cache profile := getActiveProfile(scopedID) if profile != nil { + profile.MarkUsed() return profile, nil } @@ -266,7 +283,6 @@ func GetProfileByScopedID(scopedID string) (*Profile, error) { // lock for prepping profile.Lock() - defer profile.Unlock() // prepare config err = profile.prepConfig() @@ -280,7 +296,13 @@ func GetProfileByScopedID(scopedID string) (*Profile, error) { log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err) } + // mark as internal + profile.internalSave = true + + profile.Unlock() + // mark active + profile.MarkUsed() markProfileActive(profile) return profile, nil diff --git a/profile/special.go b/profile/special.go new file mode 100644 index 00000000..13993337 --- /dev/null +++ b/profile/special.go @@ -0,0 +1,54 @@ +package profile + +import ( + "github.com/safing/portbase/log" +) + +const ( + unidentifiedProfileID = "_unidentified" + systemProfileID = "_system" +) + +func GetUnidentifiedProfile() *Profile { + // get profile + profile, err := GetProfile(SourceLocal, unidentifiedProfileID) + if err == nil { + return profile + } + + // create if not available (or error) + profile = New() + profile.Name = "Unidentified Processes" + profile.Source = SourceLocal + profile.ID = unidentifiedProfileID + + // save to db + err = profile.Save() + if err != nil { + log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err) + } + + return profile +} + +func GetSystemProfile() *Profile { + // get profile + profile, err := GetProfile(SourceLocal, systemProfileID) + if err == nil { + return profile + } + + // create if not available (or error) + profile = New() + profile.Name = "Operating System" + profile.Source = SourceLocal + profile.ID = systemProfileID + + // save to db + err = profile.Save() + if err != nil { + log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err) + } + + return profile +}