Continue with the new profile integration

This commit is contained in:
Daniel 2020-04-01 17:15:33 +02:00
parent 5523fcf0bd
commit 200d9000f6
19 changed files with 509 additions and 557 deletions

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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))

View file

@ -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

View file

@ -71,8 +71,12 @@ func GetPermittedPort() uint16 {
func portsInUseCleaner() {
for {
time.Sleep(cleanerTickDuration)
cleanPortsInUse()
select {
case <-module.Stopping():
return
case <-time.After(cleanerTickDuration):
cleanPortsInUse()
}
}
}

View file

@ -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")
}
}
}

View file

@ -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())

View file

@ -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)
}
}

View file

@ -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())
}
}

View file

@ -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,

View file

@ -42,7 +42,7 @@ const (
Outbound = false
)
// Non-Domain Connections
// Non-Domain Scopes
const (
IncomingHost = "IH"
IncomingLAN = "IL"

View file

@ -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,

29
process/config.go Normal file
View file

@ -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
}

View file

@ -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.

View file

@ -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

View file

@ -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
}

View file

@ -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()

View file

@ -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()

42
process/profile.go Normal file
View file

@ -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
}