diff --git a/firewall/api.go b/firewall/api.go index 3803126c..4b63d890 100644 --- a/firewall/api.go +++ b/firewall/api.go @@ -9,15 +9,12 @@ import ( "strconv" "strings" - "github.com/safing/portbase/utils" - "github.com/safing/portmaster/core/structure" - + "github.com/safing/portbase/api" + "github.com/safing/portbase/dataroot" "github.com/safing/portbase/log" - + "github.com/safing/portbase/utils" "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/process" - - "github.com/safing/portbase/api" ) var ( @@ -28,7 +25,7 @@ var ( ) func prepAPIAuth() error { - dataRoot = structure.Root() + dataRoot = dataroot.Root() return api.SetAuthenticator(apiAuthenticator) } diff --git a/firewall/dialer.go b/firewall/dialer.go index 325f41a1..bf158b65 100644 --- a/firewall/dialer.go +++ b/firewall/dialer.go @@ -4,12 +4,12 @@ import ( "fmt" "net" - "github.com/safing/portmaster/intel" "github.com/safing/portmaster/network/environment" + "github.com/safing/portmaster/resolver" ) func init() { - intel.SetLocalAddrFactory(PermittedAddr) + resolver.SetLocalAddrFactory(PermittedAddr) environment.SetLocalAddrFactory(PermittedAddr) } diff --git a/firewall/firewall.go b/firewall/firewall.go index 2bb49af5..dc6231a7 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -7,6 +7,9 @@ import ( "sync/atomic" "time" + "github.com/safing/portbase/config" + "github.com/safing/portbase/modules/subsystems" + "github.com/safing/portbase/log" "github.com/safing/portbase/modules" "github.com/safing/portmaster/firewall/inspection" @@ -41,11 +44,26 @@ var ( ) func init() { - module = modules.Register("firewall", prep, start, stop, "core", "network", "nameserver", "profile", "updates") + module = modules.Register("firewall", prep, start, stop, "core", "network", "resolver", "intel", "processes") + subsystems.Register( + "filter", + "Privacy Filter", + "DNS and Network Filter", + module, + "config:filter/", + &config.Option{ + Name: "Enable Privacy Filter", + Key: CfgOptionEnableFilterKey, + Description: "Enable the Privacy Filter Subsystem to filter DNS queries and network requests.", + OptType: config.OptTypeBool, + ExpertiseLevel: config.ExpertiseLevelUser, + ReleaseLevel: config.ReleaseLevelBeta, + DefaultValue: true, + }, + ) } func prep() (err error) { - err = registerConfig() if err != nil { return err @@ -188,22 +206,18 @@ func handlePacket(pkt packet.Packet) { // associate packet to link and handle link, created := network.GetOrCreateLinkByPacket(pkt) - defer func() { - go link.SaveIfNeeded() - }() if created { link.SetFirewallHandler(initialHandler) - link.HandlePacket(pkt) - return } - if link.FirewallHandlerIsSet() { - link.HandlePacket(pkt) - return - } - issueVerdict(pkt, link, 0, true) + + link.HandlePacket(pkt) } func initialHandler(pkt packet.Packet, link *network.Link) { + defer func() { + go link.SaveIfNeeded() + }() + log.Tracer(pkt.Ctx()).Trace("firewall: [initial handler]") // check for internal firewall bypass @@ -217,9 +231,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) { } else { comm.AddLink(link) } - defer func() { - go comm.SaveIfNeeded() - }() // approve link.Accept("internally approved") @@ -249,9 +260,6 @@ func initialHandler(pkt packet.Packet, link *network.Link) { return } } - defer func() { - go comm.SaveIfNeeded() - }() // add new Link to Communication (and save both) comm.AddLink(link) @@ -267,7 +275,8 @@ func initialHandler(pkt packet.Packet, link *network.Link) { log.Tracer(pkt.Ctx()).Trace("firewall: starting decision process") - DecideOnCommunication(comm, pkt) + // TODO: filter lists may have IPs in the future! + DecideOnCommunication(comm) DecideOnLink(comm, link, pkt) // TODO: link this to real status @@ -380,7 +389,7 @@ func issueVerdict(pkt packet.Packet, link *network.Link, verdict network.Verdict func run() { for { select { - case <-modules.ShuttingDown(): + case <-module.Stopping(): return case pkt := <-interception.Packets: handlePacket(pkt) @@ -391,7 +400,7 @@ func run() { func statLogger() { for { select { - case <-modules.ShuttingDown(): + case <-module.Stopping(): return case <-time.After(10 * time.Second): log.Tracef("firewall: packets accepted %d, blocked %d, dropped %d", atomic.LoadUint64(packetsAccepted), atomic.LoadUint64(packetsBlocked), atomic.LoadUint64(packetsDropped)) diff --git a/firewall/master.go b/firewall/master.go index 15040764..6ab79820 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -4,19 +4,20 @@ import ( "fmt" "net" "os" + "path/filepath" "strings" - "github.com/miekg/dns" "github.com/safing/portbase/log" - "github.com/safing/portmaster/intel" "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/process" "github.com/safing/portmaster/profile" - "github.com/safing/portmaster/status" + "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portmaster/resolver" "github.com/agext/levenshtein" + "github.com/miekg/dns" ) // Call order: @@ -30,11 +31,10 @@ import ( // 4. DecideOnLink // is called when when the first packet of a link arrives only if communication has verdict UNDECIDED or CANTSAY -// DecideOnCommunicationBeforeIntel makes a decision about a communication before the dns query is resolved and intel is gathered. -func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) { - - // check if communication needs reevaluation - if comm.NeedsReevaluation() { +// DecideOnCommunicationBeforeDNS makes a decision about a communication before the dns query is resolved and intel is gathered. +func DecideOnCommunicationBeforeDNS(comm *network.Communication) { + // update profiles and check if communication needs reevaluation + if comm.UpdateAndCheck() { log.Infof("firewall: re-evaluating verdict on %s", comm) comm.ResetVerdict() } @@ -51,116 +51,74 @@ func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) return } - // get and check profile set - profileSet := comm.Process().ProfileSet() - if profileSet == nil { - log.Errorf("firewall: denying communication %s, no Profile Set", comm) - comm.Deny("no Profile Set") - return - } - profileSet.Update(status.ActiveSecurityLevel()) + // get profile + p := comm.Process().Profile() // check for any network access - if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) { + if p.BlockScopeInternet() && p.BlockScopeLAN() { log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm) comm.Deny("accessing Internet or LAN not permitted") return } + // continueing with access to either Internet or LAN // check endpoint list - result, reason := profileSet.CheckEndpointDomain(fqdn) + // FIXME: comm.Entity.Lock() + result, reason := p.MatchEndpoint(comm.Entity) + // FIXME: comm.Entity.Unlock() switch result { - case profile.NoMatch: - comm.UpdateVerdict(network.VerdictUndecided) - if profileSet.GetProfileMode() == profile.Whitelist { - log.Infof("firewall: denying communication %s, domain is not whitelisted", comm) - comm.Deny("domain is not whitelisted") - } - case profile.Undeterminable: + case endpoints.Undeterminable: comm.UpdateVerdict(network.VerdictUndeterminable) - case profile.Denied: - log.Infof("firewall: denying communication %s, endpoint is blacklisted: %s", comm, reason) - comm.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) - case profile.Permitted: - log.Infof("firewall: permitting communication %s, endpoint is whitelisted: %s", comm, reason) - comm.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) - } -} - -// DecideOnCommunicationAfterIntel makes a decision about a communication after the dns query is resolved and intel is gathered. -func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, rrCache *intel.RRCache) { - // rrCache may be nil, when function is called for re-evaluation by DecideOnCommunication - - // check if need to run - if comm.GetVerdict() != network.VerdictUndecided { + return + case endpoints.Denied: + log.Infof("firewall: denying communication %s, domain is blacklisted: %s", comm, reason) + comm.Deny(fmt.Sprintf("domain is blacklisted: %s", reason)) + return + case endpoints.Permitted: + log.Infof("firewall: permitting communication %s, domain is whitelisted: %s", comm, reason) + comm.Accept(fmt.Sprintf("domain is whitelisted: %s", reason)) return } + // continueing with result == NoMatch - // grant self - should not get here - if comm.Process().Pid == os.Getpid() { - log.Infof("firewall: granting own communication %s", comm) - comm.Accept("") + // check default action + if p.DefaultAction() == profile.DefaultActionPermit { + log.Infof("firewall: permitting communication %s, domain is not blacklisted (default=permit)", comm) + comm.Accept("domain is not blacklisted (default=permit)") return } - // check if there is a profile - profileSet := comm.Process().ProfileSet() - if profileSet == nil { - log.Errorf("firewall: denying communication %s, no Profile Set", comm) - comm.Deny("no Profile Set") - return - } - profileSet.Update(status.ActiveSecurityLevel()) - - // TODO: Stamp integration - - switch profileSet.GetProfileMode() { - case profile.Whitelist: - log.Infof("firewall: denying communication %s, domain is not whitelisted", comm) - comm.Deny("domain is not whitelisted") - return - case profile.Blacklist: - log.Infof("firewall: permitting communication %s, domain is not blacklisted", comm) - comm.Accept("domain is not blacklisted") - return - } - - // ProfileMode == Prompt - // check relation - if profileSet.CheckFlag(profile.Related) { - if checkRelation(comm, fqdn) { + if !p.DisableAutoPermit() { + if checkRelation(comm) { return } } // prompt - prompt(comm, nil, nil, fqdn) + if p.DefaultAction() == profile.DefaultActionAsk { + prompt(comm, nil, nil) + return + } + + // DefaultAction == DefaultActionBlock + log.Infof("firewall: denying communication %s, domain is not whitelisted (default=block)", comm) + comm.Deny("domain is not whitelisted (default=block)") + return } // FilterDNSResponse filters a dns response according to the application profile and settings. -//nolint:gocognit // FIXME -func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *intel.RRCache) *intel.RRCache { +func FilterDNSResponse(comm *network.Communication, q *resolver.Query, rrCache *resolver.RRCache) *resolver.RRCache { //nolint:gocognit // TODO // do not modify own queries - this should not happen anyway if comm.Process().Pid == os.Getpid() { return rrCache } - // check if there is a profile - profileSet := comm.Process().ProfileSet() - if profileSet == nil { - log.Infof("firewall: blocking dns query of communication %s, no Profile Set", comm) - return nil - } - profileSet.Update(status.ActiveSecurityLevel()) - - // save config for consistency during function call - secLevel := profileSet.SecurityLevel() - filterByScope := filterDNSByScope(secLevel) - filterByProfile := filterDNSByProfile(secLevel) + // get profile + p := comm.Process().Profile() // check if DNS response filtering is completely turned off - if !filterByScope && !filterByProfile { + if !p.RemoveOutOfScopeDNS() && !p.RemoveBlockedDNS() { return rrCache } @@ -175,7 +133,6 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int // loop vars var classification int8 var ip net.IP - var result profile.EPResult // filter function filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) { @@ -196,7 +153,7 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int } classification = netutils.ClassifyIP(ip) - if filterByScope { + if p.RemoveOutOfScopeDNS() { switch { case classification == netutils.HostLocal: // No DNS should return localhost addresses @@ -211,30 +168,24 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int } } - if filterByProfile { + if p.RemoveBlockedDNS() { // filter by flags switch { - case !profileSet.CheckFlag(profile.Internet) && classification == netutils.Global: + case p.BlockScopeInternet() && classification == netutils.Global: addressesRemoved++ rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) continue - case !profileSet.CheckFlag(profile.LAN) && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): addressesRemoved++ rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) continue - case !profileSet.CheckFlag(profile.Localhost) && classification == netutils.HostLocal: + case p.BlockScopeLocal() && classification == netutils.HostLocal: addressesRemoved++ rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) continue } - // filter by endpoints - result, _ = profileSet.CheckEndpointIP(q.FQDN, ip, 0, 0, false) - if result == profile.Denied { - addressesRemoved++ - rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String()) - continue - } + // TODO: filter by endpoint list (IP only) } // if survived, add to good entries @@ -267,17 +218,15 @@ func FilterDNSResponse(comm *network.Communication, q *intel.Query, rrCache *int } // DecideOnCommunication makes a decision about a communication with its first packet. -func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { - - // check if communication needs reevaluation, if it's not with a domain - if comm.NeedsReevaluation() { +func DecideOnCommunication(comm *network.Communication) { + // update profiles and check if communication needs reevaluation + if comm.UpdateAndCheck() { log.Infof("firewall: re-evaluating verdict on %s", comm) comm.ResetVerdict() - // if communicating with a domain entity, re-evaluate with Before/AfterIntel - if strings.HasSuffix(comm.Domain, ".") { - DecideOnCommunicationBeforeIntel(comm, comm.Domain) - DecideOnCommunicationAfterIntel(comm, comm.Domain, nil) + // if communicating with a domain entity, re-evaluate with BeforeDNS + if strings.HasSuffix(comm.Scope, ".") { + DecideOnCommunicationBeforeDNS(comm) } } @@ -293,29 +242,24 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { return } - // check if there is a profile - profileSet := comm.Process().ProfileSet() - if profileSet == nil { - log.Errorf("firewall: denying communication %s, no Profile Set", comm) - comm.Deny("no Profile Set") - return - } - profileSet.Update(status.ActiveSecurityLevel()) + // get profile + p := comm.Process().Profile() // check comm type - switch comm.Domain { + switch comm.Scope { case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid: - if !profileSet.CheckFlag(profile.Service) { + if p.BlockInbound() { log.Infof("firewall: denying communication %s, not a service", comm) - if comm.Domain == network.IncomingHost { + if comm.Scope == network.IncomingHost { comm.Block("not a service") } else { comm.Deny("not a service") } return } - case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing! - if !profileSet.CheckFlag(profile.PeerToPeer) { + case network.PeerLAN, network.PeerInternet, network.PeerInvalid: + // Important: PeerHost is and should be missing! + if p.BlockP2P() { log.Infof("firewall: denying communication %s, peer to peer comms (to an IP) not allowed", comm) comm.Deny("peer to peer comms (to an IP) not allowed") return @@ -323,21 +267,21 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { } // check network scope - switch comm.Domain { + switch comm.Scope { case network.IncomingHost: - if !profileSet.CheckFlag(profile.Localhost) { + if p.BlockScopeLocal() { log.Infof("firewall: denying communication %s, serving localhost not allowed", comm) comm.Block("serving localhost not allowed") return } case network.IncomingLAN: - if !profileSet.CheckFlag(profile.LAN) { + if p.BlockScopeLAN() { log.Infof("firewall: denying communication %s, serving LAN not allowed", comm) comm.Deny("serving LAN not allowed") return } case network.IncomingInternet: - if !profileSet.CheckFlag(profile.Internet) { + if p.BlockScopeInternet() { log.Infof("firewall: denying communication %s, serving Internet not allowed", comm) comm.Deny("serving Internet not allowed") return @@ -347,19 +291,19 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { comm.Drop("invalid IP address") return case network.PeerHost: - if !profileSet.CheckFlag(profile.Localhost) { + if p.BlockScopeLocal() { log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm) comm.Block("accessing localhost not allowed") return } case network.PeerLAN: - if !profileSet.CheckFlag(profile.LAN) { + if p.BlockScopeLAN() { log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm) comm.Deny("accessing the LAN not allowed") return } case network.PeerInternet: - if !profileSet.CheckFlag(profile.Internet) { + if p.BlockScopeInternet() { log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm) comm.Deny("accessing the Internet not allowed") return @@ -384,7 +328,7 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa return } - // check if communicating with self + // check if process is communicating with itself if comm.Process().Pid >= 0 && pkt.Info().Src.Equal(pkt.Info().Dst) { // get PID otherPid, _, err := process.GetPidByEndpoints( @@ -424,86 +368,80 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa return } - // check if there is a profile - profileSet := comm.Process().ProfileSet() - if profileSet == nil { - log.Infof("firewall: no Profile Set, denying %s", link) - link.Deny("no Profile Set") - return - } - profileSet.Update(status.ActiveSecurityLevel()) - - // get domain - var fqdn string - if strings.HasSuffix(comm.Domain, ".") { - fqdn = comm.Domain - } - - // remoteIP - var remoteIP net.IP - if comm.Direction { - remoteIP = pkt.Info().Src - } else { - remoteIP = pkt.Info().Dst - } - - // protocol and destination port - protocol := uint8(pkt.Info().Protocol) - dstPort := pkt.Info().DstPort + // get profile + p := comm.Process().Profile() // check endpoints list - result, reason := profileSet.CheckEndpointIP(fqdn, remoteIP, protocol, dstPort, comm.Direction) + var result endpoints.EPResult + var reason string + // FIXME: link.Entity.Lock() + if comm.Direction { + result, reason = p.MatchServiceEndpoint(link.Entity) + } else { + result, reason = p.MatchEndpoint(link.Entity) + } + // FIXME: link.Entity.Unlock() switch result { - case profile.Denied: + case endpoints.Denied: log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason) link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) return - case profile.Permitted: + case endpoints.Permitted: log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason) link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) return } + // continueing with result == NoMatch - // TODO: Stamp integration - - switch profileSet.GetProfileMode() { - case profile.Whitelist: - log.Infof("firewall: denying link %s: endpoint is not whitelisted", link) - link.Deny("endpoint is not whitelisted") - return - case profile.Blacklist: - log.Infof("firewall: permitting link %s: endpoint is not blacklisted", link) - link.Accept("endpoint is not blacklisted") + // implicit default=block for incoming + if comm.Direction { + log.Infof("firewall: denying link %s: endpoint is not whitelisted (incoming is always default=block)", link) + link.Deny("endpoint is not whitelisted (incoming is always default=block)") return } - // ProfileMode == Prompt + // check default action + if p.DefaultAction() == profile.DefaultActionPermit { + log.Infof("firewall: permitting link %s: endpoint is not blacklisted (default=permit)", link) + link.Accept("endpoint is not blacklisted (default=permit)") + return + } // check relation - if fqdn != "" && profileSet.CheckFlag(profile.Related) { - if checkRelation(comm, fqdn) { + if !p.DisableAutoPermit() { + if checkRelation(comm) { return } } // prompt - prompt(comm, link, pkt, fqdn) -} - -func checkRelation(comm *network.Communication, fqdn string) (related bool) { - profileSet := comm.Process().ProfileSet() - if profileSet == nil { + if p.DefaultAction() == profile.DefaultActionAsk { + prompt(comm, link, pkt) return } - // TODO: add #AI + // DefaultAction == DefaultActionBlock + log.Infof("firewall: denying link %s: endpoint is not whitelisted (default=block)", link) + link.Deny("endpoint is not whitelisted (default=block)") + return +} - pathElements := strings.Split(comm.Process().Path, "/") // FIXME: path separator +// checkRelation tries to find a relation between a process and a communication. This is for better out of the box experience and is _not_ meant to thwart intentional malware. +func checkRelation(comm *network.Communication) (related bool) { + if comm.Entity.Domain != "" { + return false + } + // don't check for unknown processes + if comm.Process().Pid < 0 { + return false + } + + pathElements := strings.Split(comm.Process().Path, string(filepath.Separator)) // only look at the last two path segments if len(pathElements) > 2 { pathElements = pathElements[len(pathElements)-2:] } - domainElements := strings.Split(fqdn, ".") + domainElements := strings.Split(comm.Entity.Domain, ".") var domainElement string var processElement string @@ -517,11 +455,6 @@ matchLoop: break matchLoop } } - if levenshtein.Match(domainElement, profileSet.UserProfile().Name, nil) > 0.5 { - related = true - processElement = profileSet.UserProfile().Name - break matchLoop - } if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 { related = true processElement = comm.Process().Name diff --git a/firewall/ports.go b/firewall/ports.go index b6b13189..60a1ceef 100644 --- a/firewall/ports.go +++ b/firewall/ports.go @@ -71,8 +71,12 @@ func GetPermittedPort() uint16 { func portsInUseCleaner() { for { - time.Sleep(cleanerTickDuration) - cleanPortsInUse() + select { + case <-module.Stopping(): + return + case <-time.After(cleanerTickDuration): + cleanPortsInUse() + } } } diff --git a/firewall/prompt.go b/firewall/prompt.go index a6cd160f..9e2cf17f 100644 --- a/firewall/prompt.go +++ b/firewall/prompt.go @@ -1,15 +1,15 @@ package firewall import ( - "context" "fmt" "time" + "github.com/safing/portmaster/profile/endpoints" + "github.com/safing/portbase/log" "github.com/safing/portbase/notifications" "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/packet" - "github.com/safing/portmaster/profile" ) const ( @@ -30,14 +30,14 @@ var ( ) //nolint:gocognit // FIXME -func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, fqdn string) { +func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet) { nTTL := time.Duration(promptTimeout()) * time.Second // first check if there is an existing notification for this. // build notification ID var nID string switch { - case comm.Direction, fqdn == "": // connection to/from IP + case comm.Direction, comm.Entity.Domain == "": // connection to/from IP if pkt == nil { log.Error("firewall: could not prompt for incoming/direct connection: missing pkt") if link != nil { @@ -47,9 +47,9 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, } return } - nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Domain, pkt.Info().RemoteIP()) + nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Scope, pkt.Info().RemoteIP()) default: // connection to domain - nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain) + nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Scope) } n := notifications.Get(nID) saveResponse := true @@ -70,7 +70,7 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, // add message and actions switch { case comm.Direction: // incoming - n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().LocalPort()) + n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", comm.Process(), link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port) n.AvailableActions = []*notifications.Action{ { ID: permitServingIP, @@ -81,8 +81,8 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, Text: "Deny", }, } - case fqdn == "": // direct connection - n.Message = fmt.Sprintf("Application %s wants to connect to %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort()) + case comm.Entity.Domain == "": // direct connection + n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", comm.Process(), link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port) n.AvailableActions = []*notifications.Action{ { ID: permitIP, @@ -94,10 +94,10 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, }, } default: // connection to domain - if pkt != nil { - n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort()) + if link != nil { + n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Entity.Domain, link.Entity.IP.String(), link.Entity.Protocol, link.Entity.Port) } else { - n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain) + n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Entity.Domain) } n.AvailableActions = []*notifications.Action{ { @@ -141,62 +141,57 @@ func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, return } - new := &profile.EndpointPermission{ - Type: profile.EptDomain, - Value: comm.Domain, - Permit: false, - Created: time.Now().Unix(), - } + // get profile + p := comm.Process().Profile() - // permission type + var ep endpoints.Endpoint switch promptResponse { - case permitDomainAll, denyDomainAll: - new.Value = "." + new.Value - case permitIP, permitServingIP, denyIP, denyServingIP: - if pkt == nil { - log.Warningf("firewall: received invalid prompt response: %s for %s", promptResponse, comm.Domain) - return + case permitDomainAll: + ep = &endpoints.EndpointDomain{ + EndpointBase: endpoints.EndpointBase{Permitted: true}, + Domain: "." + comm.Entity.Domain, } - if pkt.Info().Version == packet.IPv4 { - new.Type = profile.EptIPv4 - } else { - new.Type = profile.EptIPv6 + case permitDomainDistinct: + ep = &endpoints.EndpointDomain{ + EndpointBase: endpoints.EndpointBase{Permitted: true}, + Domain: comm.Entity.Domain, } - new.Value = pkt.Info().RemoteIP().String() + case denyDomainAll: + ep = &endpoints.EndpointDomain{ + EndpointBase: endpoints.EndpointBase{Permitted: false}, + Domain: "." + comm.Entity.Domain, + } + case denyDomainDistinct: + ep = &endpoints.EndpointDomain{ + EndpointBase: endpoints.EndpointBase{Permitted: false}, + Domain: comm.Entity.Domain, + } + case permitIP, permitServingIP: + ep = &endpoints.EndpointIP{ + EndpointBase: endpoints.EndpointBase{Permitted: true}, + IP: comm.Entity.IP, + } + case denyIP, denyServingIP: + ep = &endpoints.EndpointIP{ + EndpointBase: endpoints.EndpointBase{Permitted: false}, + IP: comm.Entity.IP, + } + default: + log.Warningf("filter: unknown prompt response: %s", promptResponse) } - // permission verdict - switch promptResponse { - case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP: - new.Permit = false - } - - // get user profile - profileSet := comm.Process().ProfileSet() - profileSet.Lock() - defer profileSet.Unlock() - userProfile := profileSet.UserProfile() - userProfile.Lock() - defer userProfile.Unlock() - - // add to correct list switch promptResponse { case permitServingIP, denyServingIP: - userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new) + p.AddServiceEndpoint(ep.String()) default: - userProfile.Endpoints = append(userProfile.Endpoints, new) + p.AddEndpoint(ep.String()) } - // save! - module.StartMicroTask(&mtSaveProfile, func(ctx context.Context) error { - return userProfile.Save("") - }) - case <-n.Expired(): if link != nil { - link.Accept("no response to prompt") + link.Deny("no response to prompt") } else { - comm.Accept("no response to prompt") + comm.Deny("no response to prompt") } } } diff --git a/network/clean.go b/network/clean.go index fd41d82f..c4f575b7 100644 --- a/network/clean.go +++ b/network/clean.go @@ -40,9 +40,9 @@ func cleanLinks() (activeComms map[string]struct{}) { for key, link := range links { // delete dead links - link.Lock() + link.lock.Lock() deleteThis := link.Ended > 0 && link.Ended < deleteOlderThan - link.Unlock() + link.lock.Unlock() if deleteThis { log.Tracef("network.clean: deleted %s (ended at %d)", link.DatabaseKey(), link.Ended) go link.Delete() @@ -51,9 +51,9 @@ func cleanLinks() (activeComms map[string]struct{}) { // not yet deleted, so its still a valid link regarding link count comm := link.Communication() - comm.Lock() + comm.lock.Lock() markActive(activeComms, comm.DatabaseKey()) - comm.Unlock() + comm.lock.Unlock() // check if link is dead found = false @@ -66,9 +66,9 @@ func cleanLinks() (activeComms map[string]struct{}) { if !found { // mark end time - link.Lock() + link.lock.Lock() link.Ended = now - link.Unlock() + link.lock.Unlock() log.Tracef("network.clean: marked %s as ended", link.DatabaseKey()) // save linkToSave := link @@ -95,9 +95,9 @@ func cleanComms(activeLinks map[string]struct{}) (activeComms map[string]struct{ _, hasLinks := activeLinks[comm.DatabaseKey()] // comm created - comm.Lock() + comm.lock.Lock() created := comm.Meta().Created - comm.Unlock() + comm.lock.Unlock() if !hasLinks && created < threshold { log.Tracef("network.clean: deleted %s", comm.DatabaseKey()) diff --git a/network/communication.go b/network/communication.go index d2324ba8..266e032d 100644 --- a/network/communication.go +++ b/network/communication.go @@ -8,48 +8,63 @@ import ( "sync" "time" + "github.com/safing/portmaster/resolver" + "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portmaster/intel" "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/process" - "github.com/safing/portmaster/profile" ) // Communication describes a logical connection between a process and a domain. //nolint:maligned // TODO: fix alignment type Communication struct { record.Base - sync.Mutex + lock sync.Mutex - Domain string + Scope string + Entity *intel.Entity Direction bool - Intel *intel.Intel - process *process.Process - Verdict Verdict - Reason string - Inspect bool + + Verdict Verdict + Reason string + ReasonID string // format source[:id[:id]] + Inspect bool + process *process.Process + profileRevisionCounter uint64 FirstLinkEstablished int64 LastLinkEstablished int64 - profileUpdateVersion uint32 - saveWhenFinished bool + saveWhenFinished bool +} + +// Lock locks the communication and the communication's Entity. +func (comm *Communication) Lock() { + comm.lock.Lock() + comm.Entity.Lock() +} + +// Lock unlocks the communication and the communication's Entity. +func (comm *Communication) Unlock() { + comm.Entity.Unlock() + comm.lock.Unlock() } // Process returns the process that owns the connection. func (comm *Communication) Process() *process.Process { - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() return comm.process } // ResetVerdict resets the verdict to VerdictUndecided. func (comm *Communication) ResetVerdict() { - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() comm.Verdict = VerdictUndecided comm.Reason = "" @@ -58,8 +73,8 @@ func (comm *Communication) ResetVerdict() { // GetVerdict returns the current verdict. func (comm *Communication) GetVerdict() Verdict { - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() return comm.Verdict } @@ -93,8 +108,8 @@ func (comm *Communication) Drop(reason string) { // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts. func (comm *Communication) UpdateVerdict(newVerdict Verdict) { - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() if newVerdict > comm.Verdict { comm.Verdict = newVerdict @@ -108,8 +123,8 @@ func (comm *Communication) SetReason(reason string) { return } - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() comm.Reason = reason comm.saveWhenFinished = true } @@ -120,8 +135,8 @@ func (comm *Communication) AddReason(reason string) { return } - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() if comm.Reason != "" { comm.Reason += " | " @@ -129,21 +144,18 @@ func (comm *Communication) AddReason(reason string) { comm.Reason += reason } -// NeedsReevaluation returns whether the decision on this communication should be re-evaluated. -func (comm *Communication) NeedsReevaluation() bool { - comm.Lock() - defer comm.Unlock() +// UpdateAndCheck updates profiles and checks whether a reevaluation is needed. +func (comm *Communication) UpdateAndCheck() (needsReevaluation bool) { + revCnt := comm.Process().Profile().Update() - oldVersion := comm.profileUpdateVersion - comm.profileUpdateVersion = profile.GetUpdateVersion() + comm.lock.Lock() + defer comm.lock.Unlock() + if comm.profileRevisionCounter != revCnt { + comm.profileRevisionCounter = revCnt + needsReevaluation = true + } - if oldVersion == 0 { - return false - } - if oldVersion != comm.profileUpdateVersion { - return true - } - return false + return } // GetCommunicationByFirstPacket returns the matching communication from the internal storage. @@ -153,25 +165,26 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) { if err != nil { return nil, err } - var domain string + var scope string // Incoming if direction { switch netutils.ClassifyIP(pkt.Info().Src) { case netutils.HostLocal: - domain = IncomingHost + scope = IncomingHost case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - domain = IncomingLAN + scope = IncomingLAN case netutils.Global, netutils.GlobalMulticast: - domain = IncomingInternet + scope = IncomingInternet case netutils.Invalid: - domain = IncomingInvalid + scope = IncomingInvalid } - communication, ok := GetCommunication(proc.Pid, domain) + communication, ok := GetCommunication(proc.Pid, scope) if !ok { communication = &Communication{ - Domain: domain, + Scope: scope, + Entity: (&intel.Entity{}).Init(), Direction: Inbound, process: proc, Inspect: true, @@ -184,7 +197,7 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) { } // get domain - ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP()) + ipinfo, err := resolver.GetIPInfo(pkt.FmtRemoteIP()) // PeerToPeer if err != nil { @@ -192,19 +205,20 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) { switch netutils.ClassifyIP(pkt.Info().Dst) { case netutils.HostLocal: - domain = PeerHost + scope = PeerHost case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - domain = PeerLAN + scope = PeerLAN case netutils.Global, netutils.GlobalMulticast: - domain = PeerInternet + scope = PeerInternet case netutils.Invalid: - domain = PeerInvalid + scope = PeerInvalid } - communication, ok := GetCommunication(proc.Pid, domain) + communication, ok := GetCommunication(proc.Pid, scope) if !ok { communication = &Communication{ - Domain: domain, + Scope: scope, + Entity: (&intel.Entity{}).Init(), Direction: Outbound, process: proc, Inspect: true, @@ -221,7 +235,10 @@ func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) { communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0]) if !ok { communication = &Communication{ - Domain: ipinfo.Domains[0], + Scope: ipinfo.Domains[0], + Entity: (&intel.Entity{ + Domain: ipinfo.Domains[0], + }).Init(), Direction: Outbound, process: proc, Inspect: true, @@ -251,7 +268,10 @@ func GetCommunicationByDNSRequest(ctx context.Context, ip net.IP, port uint16, f communication, ok := GetCommunication(proc.Pid, fqdn) if !ok { communication = &Communication{ - Domain: fqdn, + Scope: fqdn, + Entity: (&intel.Entity{ + Domain: fqdn, + }).Init(), process: proc, Inspect: true, saveWhenFinished: true, @@ -271,7 +291,7 @@ func GetCommunication(pid int, domain string) (comm *Communication, ok bool) { } func (comm *Communication) makeKey() string { - return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Domain) + return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Scope) } // SaveWhenFinished marks the Connection for saving after all current actions are finished. @@ -281,12 +301,12 @@ func (comm *Communication) SaveWhenFinished() { // SaveIfNeeded saves the Connection if it is marked for saving when finished. func (comm *Communication) SaveIfNeeded() { - comm.Lock() + comm.lock.Lock() save := comm.saveWhenFinished if save { comm.saveWhenFinished = false } - comm.Unlock() + comm.lock.Unlock() if save { err := comm.save() @@ -299,14 +319,14 @@ func (comm *Communication) SaveIfNeeded() { // Save saves the Connection object in the storage and propagates the change. func (comm *Communication) save() error { // update comm - comm.Lock() + comm.lock.Lock() if comm.process == nil { - comm.Unlock() + comm.lock.Unlock() return errors.New("cannot save connection without process") } if !comm.KeyIsSet() { - comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Domain)) + comm.SetKey(fmt.Sprintf("network:tree/%d/%s", comm.process.Pid, comm.Scope)) comm.UpdateMeta() } if comm.Meta().Deleted > 0 { @@ -315,7 +335,7 @@ func (comm *Communication) save() error { } key := comm.makeKey() comm.saveWhenFinished = false - comm.Unlock() + comm.lock.Unlock() // save comm commsLock.RLock() @@ -336,8 +356,8 @@ func (comm *Communication) save() error { func (comm *Communication) Delete() { commsLock.Lock() defer commsLock.Unlock() - comm.Lock() - defer comm.Unlock() + comm.lock.Lock() + defer comm.lock.Unlock() delete(comms, comm.makeKey()) @@ -347,16 +367,18 @@ func (comm *Communication) Delete() { // AddLink applies the Communication to the Link and sets timestamps. func (comm *Communication) AddLink(link *Link) { + comm.lock.Lock() + defer comm.lock.Unlock() + // apply comm to link - link.Lock() + link.lock.Lock() link.comm = comm link.Verdict = comm.Verdict link.Inspect = comm.Inspect + // FIXME: use new copy methods + link.Entity.Domain = comm.Entity.Domain link.saveWhenFinished = true - link.Unlock() - - // update comm LastLinkEstablished - comm.Lock() + link.lock.Unlock() // check if we should save if comm.LastLinkEstablished < time.Now().Add(-3*time.Second).Unix() { @@ -368,8 +390,6 @@ func (comm *Communication) AddLink(link *Link) { if comm.FirstLinkEstablished == 0 { comm.FirstLinkEstablished = comm.LastLinkEstablished } - - comm.Unlock() } // String returns a string representation of Communication. @@ -377,7 +397,7 @@ func (comm *Communication) String() string { comm.Lock() defer comm.Unlock() - switch comm.Domain { + switch comm.Scope { case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid: if comm.process == nil { return "? <- *" @@ -390,8 +410,8 @@ func (comm *Communication) String() string { return fmt.Sprintf("%s -> *", comm.process.String()) default: if comm.process == nil { - return fmt.Sprintf("? -> %s", comm.Domain) + return fmt.Sprintf("? -> %s", comm.Scope) } - return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Domain) + return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Scope) } } diff --git a/network/link.go b/network/link.go index 82f02a6f..94784def 100644 --- a/network/link.go +++ b/network/link.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/safing/portmaster/intel" + "github.com/safing/portbase/database/record" "github.com/safing/portbase/log" "github.com/safing/portmaster/network/packet" @@ -16,21 +18,22 @@ import ( type FirewallHandler func(pkt packet.Packet, link *Link) // Link describes a distinct physical connection (e.g. TCP connection) - like an instance - of a Connection. -//nolint:maligned // TODO: fix alignment -type Link struct { +type Link struct { //nolint:maligned // TODO: fix alignment record.Base - sync.Mutex + lock sync.Mutex - ID string + ID string + Entity *intel.Entity + Direction bool Verdict Verdict Reason string + ReasonID string // format source[:id[:id]] Tunneled bool VerdictPermanent bool Inspect bool Started int64 Ended int64 - RemoteAddress string pktQueue chan packet.Packet firewallHandler FirewallHandler @@ -41,70 +44,82 @@ type Link struct { saveWhenFinished bool } +// Lock locks the link and the link's Entity. +func (link *Link) Lock() { + link.lock.Lock() + link.Entity.Lock() +} + +// Lock unlocks the link and the link's Entity. +func (link *Link) Unlock() { + link.Entity.Unlock() + link.lock.Unlock() +} + // Communication returns the Communication the Link is part of func (link *Link) Communication() *Communication { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() return link.comm } // GetVerdict returns the current verdict. func (link *Link) GetVerdict() Verdict { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() return link.Verdict } // FirewallHandlerIsSet returns whether a firewall handler is set or not func (link *Link) FirewallHandlerIsSet() bool { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() return link.firewallHandler != nil } // SetFirewallHandler sets the firewall handler for this link func (link *Link) SetFirewallHandler(handler FirewallHandler) { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() if link.firewallHandler == nil { - link.firewallHandler = handler link.pktQueue = make(chan packet.Packet, 1000) // start handling - module.StartWorker("", func(ctx context.Context) error { + module.StartWorker("packet handler", func(ctx context.Context) error { link.packetHandler() return nil }) - - return } link.firewallHandler = handler } // StopFirewallHandler unsets the firewall handler func (link *Link) StopFirewallHandler() { - link.Lock() + link.lock.Lock() link.firewallHandler = nil - link.Unlock() + link.lock.Unlock() link.pktQueue <- nil } // HandlePacket queues packet of Link for handling func (link *Link) HandlePacket(pkt packet.Packet) { - link.Lock() - defer link.Unlock() + // get handler + link.lock.Lock() + handler := link.firewallHandler + link.lock.Unlock() - if link.firewallHandler != nil { + // send to queue + if handler != nil { link.pktQueue <- pkt return } + // no handler! log.Warningf("network: link %s does not have a firewallHandler, dropping packet", link) - err := pkt.Drop() if err != nil { log.Warningf("network: failed to drop packet %s: %s", pkt, err) @@ -119,7 +134,7 @@ func (link *Link) Accept(reason string) { // Deny blocks or drops the link depending on the connection direction and adds the given reason. func (link *Link) Deny(reason string) { - if link.comm != nil && link.comm.Direction { + if link.Direction { link.Drop(reason) } else { link.Block(reason) @@ -151,8 +166,8 @@ func (link *Link) RerouteToTunnel(reason string) { // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts func (link *Link) UpdateVerdict(newVerdict Verdict) { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() if newVerdict > link.Verdict { link.Verdict = newVerdict @@ -166,8 +181,8 @@ func (link *Link) AddReason(reason string) { return } - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() if link.Reason != "" { link.Reason += " | " @@ -185,9 +200,9 @@ func (link *Link) packetHandler() { return } // get handler - link.Lock() + link.lock.Lock() handler := link.firewallHandler - link.Unlock() + link.lock.Unlock() // execute handler or verdict if handler != nil { handler(pkt, link) @@ -201,8 +216,8 @@ func (link *Link) packetHandler() { // ApplyVerdict appies the link verdict to a packet. func (link *Link) ApplyVerdict(pkt packet.Packet) { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() var err error @@ -251,12 +266,12 @@ func (link *Link) SaveWhenFinished() { // SaveIfNeeded saves the Link if it is marked for saving when finished. func (link *Link) SaveIfNeeded() { - link.Lock() + link.lock.Lock() save := link.saveWhenFinished if save { link.saveWhenFinished = false } - link.Unlock() + link.lock.Unlock() if save { link.saveAndLog() @@ -274,18 +289,18 @@ func (link *Link) saveAndLog() { // save saves the link object in the storage and propagates the change. func (link *Link) save() error { // update link - link.Lock() + link.lock.Lock() if link.comm == nil { - link.Unlock() + link.lock.Unlock() return errors.New("cannot save link without comms") } if !link.KeyIsSet() { - link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Domain, link.ID)) + link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Scope, link.ID)) link.UpdateMeta() } link.saveWhenFinished = false - link.Unlock() + link.lock.Unlock() // save link linksLock.RLock() @@ -306,8 +321,8 @@ func (link *Link) save() error { func (link *Link) Delete() { linksLock.Lock() defer linksLock.Unlock() - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() delete(links, link.ID) @@ -339,10 +354,15 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) { // CreateLinkFromPacket creates a new Link based on Packet. func CreateLinkFromPacket(pkt packet.Packet) *Link { link := &Link{ - ID: pkt.GetLinkID(), + ID: pkt.GetLinkID(), + Entity: (&intel.Entity{ + IP: pkt.Info().RemoteIP(), + Protocol: uint8(pkt.Info().Protocol), + Port: pkt.Info().RemotePort(), + }).Init(), + Direction: pkt.IsInbound(), Verdict: VerdictUndecided, Started: time.Now().Unix(), - RemoteAddress: pkt.FmtRemoteAddress(), saveWhenFinished: true, } return link @@ -350,59 +370,59 @@ func CreateLinkFromPacket(pkt packet.Packet) *Link { // GetActiveInspectors returns the list of active inspectors. func (link *Link) GetActiveInspectors() []bool { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() return link.activeInspectors } // SetActiveInspectors sets the list of active inspectors. func (link *Link) SetActiveInspectors(new []bool) { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() link.activeInspectors = new } // GetInspectorData returns the list of inspector data. func (link *Link) GetInspectorData() map[uint8]interface{} { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() return link.inspectorData } // SetInspectorData set the list of inspector data. func (link *Link) SetInspectorData(new map[uint8]interface{}) { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() link.inspectorData = new } // String returns a string representation of Link. func (link *Link) String() string { - link.Lock() - defer link.Unlock() + link.lock.Lock() + defer link.lock.Unlock() if link.comm == nil { - return fmt.Sprintf("? <-> %s", link.RemoteAddress) + return fmt.Sprintf("? <-> %s", link.Entity.IP.String()) } - switch link.comm.Domain { - case "I": + switch link.comm.Scope { + case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid: if link.comm.process == nil { - return fmt.Sprintf("? <- %s", link.RemoteAddress) + return fmt.Sprintf("? <- %s", link.Entity.IP.String()) } - return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.RemoteAddress) - case "D": + return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.Entity.IP.String()) + case PeerHost, PeerLAN, PeerInternet, PeerInvalid: if link.comm.process == nil { - return fmt.Sprintf("? -> %s", link.RemoteAddress) + return fmt.Sprintf("? -> %s", link.Entity.IP.String()) } - return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.RemoteAddress) + return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.Entity.IP.String()) default: if link.comm.process == nil { - return fmt.Sprintf("? -> %s (%s)", link.comm.Domain, link.RemoteAddress) + return fmt.Sprintf("? -> %s (%s)", link.comm.Scope, link.Entity.IP.String()) } - return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Domain, link.RemoteAddress) + return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Scope, link.Entity.IP.String()) } } diff --git a/network/self.go b/network/self.go index 60213855..b57bd981 100644 --- a/network/self.go +++ b/network/self.go @@ -5,36 +5,38 @@ import ( "os" "time" + "github.com/safing/portmaster/intel" "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/process" ) -// GetOwnComm returns the communication for the given packet, that originates from +// GetOwnComm returns the communication for the given packet, that originates from the Portmaster itself. func GetOwnComm(pkt packet.Packet) (*Communication, error) { - var domain string + var scope string // Incoming if pkt.IsInbound() { switch netutils.ClassifyIP(pkt.Info().RemoteIP()) { case netutils.HostLocal: - domain = IncomingHost + scope = IncomingHost case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - domain = IncomingLAN + scope = IncomingLAN case netutils.Global, netutils.GlobalMulticast: - domain = IncomingInternet + scope = IncomingInternet case netutils.Invalid: - domain = IncomingInvalid + scope = IncomingInvalid } - communication, ok := GetCommunication(os.Getpid(), domain) + communication, ok := GetCommunication(os.Getpid(), scope) if !ok { proc, err := process.GetOrFindProcess(pkt.Ctx(), os.Getpid()) if err != nil { return nil, fmt.Errorf("could not get own process") } communication = &Communication{ - Domain: domain, + Scope: scope, + Entity: (&intel.Entity{}).Init(), Direction: Inbound, process: proc, Inspect: true, @@ -48,23 +50,24 @@ func GetOwnComm(pkt packet.Packet) (*Communication, error) { // PeerToPeer switch netutils.ClassifyIP(pkt.Info().RemoteIP()) { case netutils.HostLocal: - domain = PeerHost + scope = PeerHost case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: - domain = PeerLAN + scope = PeerLAN case netutils.Global, netutils.GlobalMulticast: - domain = PeerInternet + scope = PeerInternet case netutils.Invalid: - domain = PeerInvalid + scope = PeerInvalid } - communication, ok := GetCommunication(os.Getpid(), domain) + communication, ok := GetCommunication(os.Getpid(), scope) if !ok { proc, err := process.GetOrFindProcess(pkt.Ctx(), os.Getpid()) if err != nil { return nil, fmt.Errorf("could not get own process") } communication = &Communication{ - Domain: domain, + Scope: scope, + Entity: (&intel.Entity{}).Init(), Direction: Outbound, process: proc, Inspect: true, diff --git a/network/status.go b/network/status.go index 16534025..04781283 100644 --- a/network/status.go +++ b/network/status.go @@ -42,7 +42,7 @@ const ( Outbound = false ) -// Non-Domain Connections +// Non-Domain Scopes const ( IncomingHost = "IH" IncomingLAN = "IL" diff --git a/network/unknown.go b/network/unknown.go index 98c47ee4..7277b40d 100644 --- a/network/unknown.go +++ b/network/unknown.go @@ -3,6 +3,7 @@ package network import ( "time" + "github.com/safing/portmaster/intel" "github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/process" @@ -43,11 +44,12 @@ func GetUnknownCommunication(pkt packet.Packet) (*Communication, error) { return getOrCreateUnknownCommunication(pkt, PeerInvalid) } -func getOrCreateUnknownCommunication(pkt packet.Packet, connClass string) (*Communication, error) { - connection, ok := GetCommunication(process.UnknownProcess.Pid, connClass) +func getOrCreateUnknownCommunication(pkt packet.Packet, connScope string) (*Communication, error) { + connection, ok := GetCommunication(process.UnknownProcess.Pid, connScope) if !ok { connection = &Communication{ - Domain: connClass, + Scope: connScope, + Entity: (&intel.Entity{}).Init(), Direction: pkt.IsInbound(), Verdict: VerdictDrop, Reason: ReasonUnknownProcess, diff --git a/process/config.go b/process/config.go new file mode 100644 index 00000000..36722fe5 --- /dev/null +++ b/process/config.go @@ -0,0 +1,29 @@ +package process + +import ( + "github.com/safing/portbase/config" +) + +var ( + CfgOptionEnableProcessDetectionKey = "core/enableProcessDetection" + enableProcessDetection config.BoolOption +) + +func registerConfiguration() error { + // Enable Process Detection + // This should be always enabled. Provided as an option to disable in case there are severe problems on a system, or for debugging. + err := config.Register(&config.Option{ + Name: "Enable Process Detection", + Key: CfgOptionEnableProcessDetectionKey, + Description: "This option enables the attribution of network traffic to processes. This should be always enabled, and effectively disables app profiles if disabled.", + OptType: config.OptTypeBool, + ExpertiseLevel: config.ExpertiseLevelDeveloper, + DefaultValue: true, + }) + if err != nil { + return err + } + enableProcessDetection = config.Concurrent.GetAsBool(CfgOptionEnableProcessDetectionKey, true) + + return nil +} diff --git a/process/database.go b/process/database.go index d86f79b2..e7240af2 100644 --- a/process/database.go +++ b/process/database.go @@ -9,7 +9,6 @@ import ( "github.com/safing/portbase/database" "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile" "github.com/tevino/abool" ) @@ -90,11 +89,7 @@ func (p *Process) Delete() { go dbController.PushUpdate(p) } - // deactivate profile - // TODO: check if there is another process using the same profile set - if p.profileSet != nil { - profile.DeactivateProfileSet(p.profileSet) - } + // TODO: maybe mark the assigned profiles as no longer needed? } // CleanProcessStorage cleans the storage from old processes. diff --git a/process/find.go b/process/find.go index 6a26e00a..997d487b 100644 --- a/process/find.go +++ b/process/find.go @@ -56,6 +56,11 @@ func GetPidByPacket(pkt packet.Packet) (pid int, direction bool, err error) { // GetProcessByPacket returns the process that owns the given packet. 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 + } + log.Tracer(pkt.Ctx()).Tracef("process: getting process and profile by packet") var pid int @@ -75,10 +80,9 @@ func GetProcessByPacket(pkt packet.Packet) (process *Process, direction bool, er return nil, direction, err } - err = process.FindProfiles(pkt.Ctx()) + err = process.GetProfile(pkt.Ctx()) if err != nil { - log.Tracer(pkt.Ctx()).Errorf("process: failed to find profiles for process %s: %s", process, err) - log.Errorf("failed to find profiles for process %s: %s", process, err) + log.Tracer(pkt.Ctx()).Errorf("process: failed to get profile for process %s: %s", process, err) } return process, direction, nil @@ -110,6 +114,11 @@ func GetPidByEndpoints(localIP net.IP, localPort uint16, remoteIP net.IP, remote // GetProcessByEndpoints returns the process that owns the described link. 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 + } + log.Tracer(ctx).Tracef("process: getting process and profile by endpoints") var pid int @@ -129,10 +138,9 @@ func GetProcessByEndpoints(ctx context.Context, localIP net.IP, localPort uint16 return nil, err } - err = process.FindProfiles(ctx) + err = process.GetProfile(ctx) if err != nil { - log.Tracer(ctx).Errorf("process: failed to find profiles for process %s: %s", process, err) - log.Errorf("process: failed to find profiles for process %s: %s", process, err) + log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err) } return process, nil diff --git a/process/matching.go b/process/matching.go deleted file mode 100644 index 88c020c6..00000000 --- a/process/matching.go +++ /dev/null @@ -1,108 +0,0 @@ -package process - -import ( - "context" - "fmt" - - "github.com/safing/portbase/database" - "github.com/safing/portbase/database/query" - "github.com/safing/portbase/log" - "github.com/safing/portmaster/profile" -) - -var ( - profileDB = database.NewInterface(nil) -) - -// FindProfiles finds and assigns a profile set to the process. -func (p *Process) FindProfiles(ctx context.Context) error { - log.Tracer(ctx).Trace("process: loading profile set") - - p.Lock() - defer p.Unlock() - - // only find profiles if not already done. - if p.profileSet != nil { - return nil - } - - // User Profile - it, err := profileDB.Query(query.New(profile.MakeProfileKey(profile.UserNamespace, "")).Where(query.Where("LinkedPath", query.SameAs, p.Path))) - if err != nil { - return err - } - - var userProfile *profile.Profile - // get first result - r := <-it.Next - // cancel immediately - it.Cancel() - // ensure its a profile - userProfile, err = profile.EnsureProfile(r) - if err != nil { - return err - } - - // create new profile if it does not exist. - if userProfile == nil { - // create new profile - userProfile = profile.New() - userProfile.Name = p.ExecName - userProfile.LinkedPath = p.Path - } - - if userProfile.MarkUsed() { - _ = userProfile.Save(profile.UserNamespace) - } - - // Stamp - // Find/Re-evaluate Stamp profile - // 1. check linked stamp profile - // 2. if last check is was more than a week ago, fetch from stamp: - // 3. send path identifier to stamp - // 4. evaluate all returned profiles - // 5. select best - // 6. link stamp profile to user profile - // FIXME: implement! - - p.UserProfileKey = userProfile.Key() - p.profileSet = profile.NewSet(ctx, fmt.Sprintf("%d-%s", p.Pid, p.Path), userProfile, nil) - go p.Save() - - return nil -} - -//nolint:deadcode,unused // FIXME -func matchProfile(p *Process, prof *profile.Profile) (score int) { - for _, fp := range prof.Fingerprints { - score += matchFingerprint(p, fp) - } - return -} - -//nolint:deadcode,unused // FIXME -func matchFingerprint(p *Process, fp *profile.Fingerprint) (score int) { - if !fp.MatchesOS() { - return 0 - } - - switch fp.Type { - case "full_path": - if p.Path == fp.Value { - return profile.GetFingerprintWeight(fp.Type) - } - case "partial_path": - // FIXME: if full_path matches, do not match partial paths - return profile.GetFingerprintWeight(fp.Type) - case "md5_sum", "sha1_sum", "sha256_sum": - // FIXME: one sum is enough, check sums in a grouped form, start with the best - sum, err := p.GetExecHash(fp.Type) - if err != nil { - log.Errorf("process: failed to get hash of executable: %s", err) - } else if sum == fp.Value { - return profile.GetFingerprintWeight(fp.Type) - } - } - - return 0 -} diff --git a/process/proc/processfinder.go b/process/proc/processfinder.go index 32f25136..f6f57408 100644 --- a/process/proc/processfinder.go +++ b/process/proc/processfinder.go @@ -17,7 +17,7 @@ var ( ) // GetPidOfInode returns the pid of the given uid and socket inode. -func GetPidOfInode(uid, inode int) (int, bool) { +func GetPidOfInode(uid, inode int) (int, bool) { //nolint:gocognit // TODO pidsByUserLock.Lock() defer pidsByUserLock.Unlock() diff --git a/process/process.go b/process/process.go index fa145586..608c39ad 100644 --- a/process/process.go +++ b/process/process.go @@ -40,10 +40,10 @@ type Process struct { // ExecOwner ... // ExecSignature ... - UserProfileKey string - profileSet *profile.Set - Name string - Icon string + LocalProfileKey string + profile *profile.LayeredProfile + Name string + Icon string // Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache. FirstCommEstablished int64 @@ -53,12 +53,12 @@ type Process struct { Error string // If this is set, the process is invalid. This is used to cache failing or inexistent processes. } -// ProfileSet returns the assigned profile set. -func (p *Process) ProfileSet() *profile.Set { +// Profile returns the assigned layered profile. +func (p *Process) Profile() *profile.LayeredProfile { p.Lock() defer p.Unlock() - return p.profileSet + return p.profile } // Strings returns a string representation of process. @@ -208,13 +208,14 @@ func GetOrFindProcess(ctx context.Context, pid int) (*Process, error) { func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) { dupReqLock.Lock() - defer dupReqLock.Unlock() // get duplicate request waitgroup wg, requestActive := dupReqMap[pid] // someone else is already on it! if requestActive { + dupReqLock.Unlock() + // log that we are waiting log.Tracer(ctx).Tracef("intel: waiting for duplicate request for PID %d to complete", pid) // wait @@ -232,6 +233,8 @@ func deduplicateRequest(ctx context.Context, pid int) (finishRequest func()) { // add to registry dupReqMap[pid] = wg + dupReqLock.Unlock() + // return function to mark request as finished return func() { dupReqLock.Lock() diff --git a/process/profile.go b/process/profile.go new file mode 100644 index 00000000..388684fa --- /dev/null +++ b/process/profile.go @@ -0,0 +1,42 @@ +package process + +import ( + "context" + + "github.com/safing/portbase/log" + "github.com/safing/portmaster/profile" +) + +// GetProfile finds and assigns a profile set to the process. +func (p *Process) GetProfile(ctx context.Context) error { + p.Lock() + defer p.Unlock() + + // only find profiles if not already done. + if p.profile != nil { + log.Tracer(ctx).Trace("process: profile already loaded") + return nil + } + log.Tracer(ctx).Trace("process: loading profile") + + // get profile + localProfile, new, err := profile.FindOrCreateLocalProfileByPath(p.Path) + if err != nil { + return err + } + // add more information if new + if new { + localProfile.Name = p.ExecName + } + + // mark as used and save + if localProfile.MarkUsed() { + _ = localProfile.Save() + } + + p.LocalProfileKey = localProfile.Key() + p.profile = profile.NewLayeredProfile(localProfile) + + go p.Save() + return nil +}