Reevaluate and update firewall core logic

This commit is contained in:
Daniel 2019-02-22 16:18:58 +01:00
parent d28ed664aa
commit f7a07cbb2f
39 changed files with 1469 additions and 915 deletions

View file

@ -1,4 +1,4 @@
package global package core
import ( import (
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
@ -7,11 +7,10 @@ import (
// module dependencies // module dependencies
_ "github.com/Safing/portbase/database/dbmodule" _ "github.com/Safing/portbase/database/dbmodule"
_ "github.com/Safing/portbase/database/storage/badger" _ "github.com/Safing/portbase/database/storage/badger"
_ "github.com/Safing/portmaster/status"
) )
func init() { func init() {
modules.Register("global", nil, start, nil, "database", "status") modules.Register("core", nil, start, nil, "database")
} }
func start() error { func start() error {

View file

@ -2,10 +2,13 @@ package firewall
import ( import (
"github.com/Safing/portbase/config" "github.com/Safing/portbase/config"
"github.com/Safing/portmaster/status"
) )
var ( var (
permanentVerdicts config.BoolOption permanentVerdicts config.BoolOption
filterDNSByScope status.SecurityLevelOption
filterDNSByProfile status.SecurityLevelOption
) )
func registerConfig() error { func registerConfig() error {
@ -22,5 +25,35 @@ func registerConfig() error {
} }
permanentVerdicts = config.Concurrent.GetAsBool("firewall/permanentVerdicts", true) permanentVerdicts = config.Concurrent.GetAsBool("firewall/permanentVerdicts", true)
err = config.Register(&config.Option{
Name: "Filter DNS Responses by Server Scope",
Key: "firewall/filterDNSByScope",
Description: "This option will filter out DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
filterDNSByScope = status.ConfigIsActiveConcurrent("firewall/filterDNSByScope")
err = config.Register(&config.Option{
Name: "Filter DNS Responses by Application Profile",
Key: "firewall/filterDNSByProfile",
Description: "This option will filter out DNS answers that an application would not be allowed to connect, based on its profile.",
ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt,
ExternalOptType: "security level",
DefaultValue: 7,
ValidationRegex: "^(7|6|4)$",
})
if err != nil {
return err
}
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
return nil return nil
} }

View file

@ -13,6 +13,10 @@ import (
"github.com/Safing/portmaster/firewall/interception" "github.com/Safing/portmaster/firewall/interception"
"github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network"
"github.com/Safing/portmaster/network/packet" "github.com/Safing/portmaster/network/packet"
// module dependencies
_ "github.com/Safing/portmaster/core"
_ "github.com/Safing/portmaster/profile"
) )
var ( var (
@ -37,7 +41,7 @@ var (
) )
func init() { func init() {
modules.Register("firewall", prep, start, stop, "global", "network", "nameserver", "profile") modules.Register("firewall", prep, start, stop, "core", "network", "nameserver", "profile")
} }
func prep() (err error) { func prep() (err error) {
@ -111,7 +115,7 @@ func handlePacket(pkt packet.Packet) {
return return
} }
// log.Debugf("firewall: pkt %s has ID %s", pkt, pkt.GetConnectionID()) // log.Debugf("firewall: pkt %s has ID %s", pkt, pkt.GetLinkID())
// use this to time how long it takes process packet // use this to time how long it takes process packet
// timed := time.Now() // timed := time.Now()
@ -146,43 +150,51 @@ func handlePacket(pkt packet.Packet) {
func initialHandler(pkt packet.Packet, link *network.Link) { func initialHandler(pkt packet.Packet, link *network.Link) {
// get Connection // get Communication
connection, err := network.GetConnectionByFirstPacket(pkt) comm, err := network.GetCommunicationByFirstPacket(pkt)
if err != nil { if err != nil {
// get "unknown" connection // get "unknown" comm
link.Deny(fmt.Sprintf("could not get process: %s", err)) link.Deny(fmt.Sprintf("could not get process: %s", err))
connection, err = network.GetUnknownConnection(pkt) comm, err = network.GetUnknownCommunication(pkt)
if err != nil { if err != nil {
// all failed // all failed
log.Errorf("firewall: could not get unknown connection (dropping %s): %s", pkt.String(), err) log.Errorf("firewall: could not get unknown comm (dropping %s): %s", pkt.String(), err)
link.UpdateVerdict(network.DROP) link.UpdateVerdict(network.VerdictDrop)
verdict(pkt, network.DROP) verdict(pkt, network.VerdictDrop)
link.StopFirewallHandler() link.StopFirewallHandler()
return return
} }
} }
// add new Link to Connection (and save both) // add new Link to Communication (and save both)
connection.AddLink(link) comm.AddLink(link)
// reroute dns requests to nameserver // reroute dns requests to nameserver
if connection.Process().Pid != os.Getpid() && pkt.IsOutbound() && pkt.GetTCPUDPHeader() != nil && !pkt.GetIPHeader().Dst.Equal(localhost) && pkt.GetTCPUDPHeader().DstPort == 53 { if comm.Process().Pid != os.Getpid() && pkt.IsOutbound() && pkt.GetTCPUDPHeader() != nil && !pkt.GetIPHeader().Dst.Equal(localhost) && pkt.GetTCPUDPHeader().DstPort == 53 {
link.RerouteToNameserver() link.RerouteToNameserver()
verdict(pkt, link.GetVerdict()) verdict(pkt, link.GetVerdict())
link.StopFirewallHandler() link.StopFirewallHandler()
return return
} }
// make a decision if not made already // check if communication needs reevaluation
if connection.GetVerdict() == network.UNDECIDED { if comm.NeedsReevaluation() {
DecideOnConnection(connection, pkt) comm.ResetVerdict()
} }
if connection.GetVerdict() == network.ACCEPT {
DecideOnLink(connection, link, pkt) // make a decision if not made already
} else { switch comm.GetVerdict() {
link.UpdateVerdict(connection.GetVerdict()) case network.VerdictUndecided, network.VerdictUndeterminable:
DecideOnCommunication(comm, pkt)
}
switch comm.GetVerdict() {
case network.VerdictUndecided, network.VerdictUndeterminable, network.VerdictAccept:
DecideOnLink(comm, link, pkt)
default:
link.UpdateVerdict(comm.GetVerdict())
} }
// log decision // log decision
@ -201,7 +213,7 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
// // tunnel link, don't inspect // // tunnel link, don't inspect
// link.Tunneled = true // link.Tunneled = true
// link.StopFirewallHandler() // link.StopFirewallHandler()
// permanentVerdict(pkt, network.ACCEPT) // permanentVerdict(pkt, network.VerdictAccept)
case link.Inspect: case link.Inspect:
link.SetFirewallHandler(inspectThenVerdict) link.SetFirewallHandler(inspectThenVerdict)
inspectThenVerdict(pkt, link) inspectThenVerdict(pkt, link)
@ -241,22 +253,22 @@ func inspectThenVerdict(pkt packet.Packet, link *network.Link) {
func permanentVerdict(pkt packet.Packet, action network.Verdict) { func permanentVerdict(pkt packet.Packet, action network.Verdict) {
switch action { switch action {
case network.ACCEPT: case network.VerdictAccept:
atomic.AddUint64(packetsAccepted, 1) atomic.AddUint64(packetsAccepted, 1)
pkt.PermanentAccept() pkt.PermanentAccept()
return return
case network.BLOCK: case network.VerdictBlock:
atomic.AddUint64(packetsBlocked, 1) atomic.AddUint64(packetsBlocked, 1)
pkt.PermanentBlock() pkt.PermanentBlock()
return return
case network.DROP: case network.VerdictDrop:
atomic.AddUint64(packetsDropped, 1) atomic.AddUint64(packetsDropped, 1)
pkt.PermanentDrop() pkt.PermanentDrop()
return return
case network.RerouteToNameserver: case network.VerdictRerouteToNameserver:
pkt.RerouteToNameserver() pkt.RerouteToNameserver()
return return
case network.RerouteToTunnel: case network.VerdictRerouteToTunnel:
pkt.RerouteToTunnel() pkt.RerouteToTunnel()
return return
} }
@ -265,22 +277,22 @@ func permanentVerdict(pkt packet.Packet, action network.Verdict) {
func verdict(pkt packet.Packet, action network.Verdict) { func verdict(pkt packet.Packet, action network.Verdict) {
switch action { switch action {
case network.ACCEPT: case network.VerdictAccept:
atomic.AddUint64(packetsAccepted, 1) atomic.AddUint64(packetsAccepted, 1)
pkt.Accept() pkt.Accept()
return return
case network.BLOCK: case network.VerdictBlock:
atomic.AddUint64(packetsBlocked, 1) atomic.AddUint64(packetsBlocked, 1)
pkt.Block() pkt.Block()
return return
case network.DROP: case network.VerdictDrop:
atomic.AddUint64(packetsDropped, 1) atomic.AddUint64(packetsDropped, 1)
pkt.Drop() pkt.Drop()
return return
case network.RerouteToNameserver: case network.VerdictRerouteToNameserver:
pkt.RerouteToNameserver() pkt.RerouteToNameserver()
return return
case network.RerouteToTunnel: case network.VerdictRerouteToTunnel:
pkt.RerouteToTunnel() pkt.RerouteToTunnel()
return return
} }
@ -302,26 +314,26 @@ func verdict(pkt packet.Packet, action network.Verdict) {
func logInitialVerdict(link *network.Link) { func logInitialVerdict(link *network.Link) {
// switch link.GetVerdict() { // switch link.GetVerdict() {
// case network.ACCEPT: // case network.VerdictAccept:
// log.Infof("firewall: accepting new link: %s", link.String()) // log.Infof("firewall: accepting new link: %s", link.String())
// case network.BLOCK: // case network.VerdictBlock:
// log.Infof("firewall: blocking new link: %s", link.String()) // log.Infof("firewall: blocking new link: %s", link.String())
// case network.DROP: // case network.VerdictDrop:
// log.Infof("firewall: dropping new link: %s", link.String()) // log.Infof("firewall: dropping new link: %s", link.String())
// case network.RerouteToNameserver: // case network.VerdictRerouteToNameserver:
// log.Infof("firewall: rerouting new link to nameserver: %s", link.String()) // log.Infof("firewall: rerouting new link to nameserver: %s", link.String())
// case network.RerouteToTunnel: // case network.VerdictRerouteToTunnel:
// log.Infof("firewall: rerouting new link to tunnel: %s", link.String()) // log.Infof("firewall: rerouting new link to tunnel: %s", link.String())
// } // }
} }
func logChangedVerdict(link *network.Link) { func logChangedVerdict(link *network.Link) {
// switch link.GetVerdict() { // switch link.GetVerdict() {
// case network.ACCEPT: // case network.VerdictAccept:
// log.Infof("firewall: change! - now accepting link: %s", link.String()) // log.Infof("firewall: change! - now accepting link: %s", link.String())
// case network.BLOCK: // case network.VerdictBlock:
// log.Infof("firewall: change! - now blocking link: %s", link.String()) // log.Infof("firewall: change! - now blocking link: %s", link.String())
// case network.DROP: // case network.VerdictDrop:
// log.Infof("firewall: change! - now dropping link: %s", link.String()) // log.Infof("firewall: change! - now dropping link: %s", link.String())
// } // }
} }

View file

@ -54,7 +54,7 @@ func RunInspectors(pkt packet.Packet, link *network.Link) (network.Verdict, bool
} }
continueInspection := false continueInspection := false
verdict := network.UNDECIDED verdict := network.VerdictUndecided
for key, skip := range activeInspectors { for key, skip := range activeInspectors {
@ -69,28 +69,28 @@ func RunInspectors(pkt packet.Packet, link *network.Link) (network.Verdict, bool
action := inspectors[key](pkt, link) action := inspectors[key](pkt, link)
switch action { switch action {
case DO_NOTHING: case DO_NOTHING:
if verdict < network.ACCEPT { if verdict < network.VerdictAccept {
verdict = network.ACCEPT verdict = network.VerdictAccept
} }
continueInspection = true continueInspection = true
case BLOCK_PACKET: case BLOCK_PACKET:
if verdict < network.BLOCK { if verdict < network.VerdictBlock {
verdict = network.BLOCK verdict = network.VerdictBlock
} }
continueInspection = true continueInspection = true
case DROP_PACKET: case DROP_PACKET:
verdict = network.DROP verdict = network.VerdictDrop
continueInspection = true continueInspection = true
case BLOCK_LINK: case BLOCK_LINK:
link.UpdateVerdict(network.BLOCK) link.UpdateVerdict(network.VerdictBlock)
activeInspectors[key] = true activeInspectors[key] = true
if verdict < network.BLOCK { if verdict < network.VerdictBlock {
verdict = network.BLOCK verdict = network.VerdictBlock
} }
case DROP_LINK: case DROP_LINK:
link.UpdateVerdict(network.DROP) link.UpdateVerdict(network.VerdictDrop)
activeInspectors[key] = true activeInspectors[key] = true
verdict = network.DROP verdict = network.VerdictDrop
case STOP_INSPECTING: case STOP_INSPECTING:
activeInspectors[key] = true activeInspectors[key] = true
} }

View file

@ -2,85 +2,89 @@ package firewall
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"strings" "strings"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/Safing/portmaster/intel" "github.com/Safing/portmaster/intel"
"github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/network/packet" "github.com/Safing/portmaster/network/packet"
"github.com/Safing/portmaster/profile" "github.com/Safing/portmaster/profile"
"github.com/Safing/portmaster/status" "github.com/Safing/portmaster/status"
"github.com/miekg/dns"
"github.com/agext/levenshtein" "github.com/agext/levenshtein"
) )
// Call order: // Call order:
// //
// 1. DecideOnConnectionBeforeIntel (if connecting to domain) // 1. DecideOnCommunicationBeforeIntel (if connecting to domain)
// is called when a DNS query is made, before the query is resolved // is called when a DNS query is made, before the query is resolved
// 2. DecideOnConnectionAfterIntel (if connecting to domain) // 2. DecideOnCommunicationAfterIntel (if connecting to domain)
// is called when a DNS query is made, after the query is resolved // is called when a DNS query is made, after the query is resolved
// 3. DecideOnConnection // 3. DecideOnCommunication
// is called when the first packet of the first link of the connection arrives // is called when the first packet of the first link of the communication arrives
// 4. DecideOnLink // 4. DecideOnLink
// is called when when the first packet of a link arrives only if connection has verdict UNDECIDED or CANTSAY // is called when when the first packet of a link arrives only if communication has verdict UNDECIDED or CANTSAY
// DecideOnConnectionBeforeIntel makes a decision about a connection before the dns query is resolved and intel is gathered. // DecideOnCommunicationBeforeIntel makes a decision about a communication before the dns query is resolved and intel is gathered.
func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) { func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) {
// check:
// Profile.DomainWhitelist
// Profile.Flags
// - process specific: System, Admin, User
// - network specific: Internet, LocalNet
// grant self // grant self
if connection.Process().Pid == os.Getpid() { if comm.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection) log.Infof("firewall: granting own communication %s", comm)
connection.Accept("") comm.Accept("")
return return
} }
// check if there is a profile // get and check profile set
profileSet := connection.Process().ProfileSet() profileSet := comm.Process().ProfileSet()
if profileSet == nil { if profileSet == nil {
log.Errorf("firewall: denying connection %s, no Profile Set", connection) log.Errorf("firewall: denying communication %s, no Profile Set", comm)
connection.Deny("no Profile Set") comm.Deny("no Profile Set")
return return
} }
profileSet.Update(status.ActiveSecurityLevel()) profileSet.Update(status.ActiveSecurityLevel())
// check for any network access // check for any network access
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) { if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, accessing Internet or LAN not allowed", connection) log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm)
connection.Deny("accessing Internet or LAN not allowed") comm.Deny("accessing Internet or LAN not permitted")
return return
} }
// check domain list // check endpoint list
permitted, reason, ok := profileSet.CheckEndpoint(fqdn, 0, 0, false) result, reason := profileSet.CheckEndpointDomain(fqdn)
if ok { switch result {
if permitted { case profile.NoMatch:
log.Infof("firewall: accepting connection %s, endpoint is whitelisted: %s", connection, reason) comm.UpdateVerdict(network.VerdictUndecided)
connection.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason)) case profile.Undeterminable:
} else { comm.UpdateVerdict(network.VerdictUndeterminable)
log.Infof("firewall: denying connection %s, endpoint is blacklisted: %s", connection, reason) return
connection.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason)) case profile.Denied:
} log.Infof("firewall: denying communication %s, endpoint is blacklisted: %s", comm, reason)
comm.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
return
case profile.Permitted:
log.Infof("firewall: permitting communication %s, endpoint is whitelisted: %s", comm, reason)
comm.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
return return
} }
switch profileSet.GetProfileMode() { switch profileSet.GetProfileMode() {
case profile.Whitelist: case profile.Whitelist:
log.Infof("firewall: denying connection %s, domain is not whitelisted", connection) log.Infof("firewall: denying communication %s, domain is not whitelisted", comm)
connection.Deny("domain is not whitelisted") comm.Deny("domain is not whitelisted")
return
case profile.Prompt: case profile.Prompt:
// check Related flag // check Related flag
// TODO: improve this! // TODO: improve this!
if profileSet.CheckFlag(profile.Related) { if profileSet.CheckFlag(profile.Related) {
matched := false matched := false
pathElements := strings.Split(connection.Process().Path, "/") // FIXME: path seperator pathElements := strings.Split(comm.Process().Path, "/") // FIXME: path seperator
// only look at the last two path segments // only look at the last two path segments
if len(pathElements) > 2 { if len(pathElements) > 2 {
pathElements = pathElements[len(pathElements)-2:] pathElements = pathElements[len(pathElements)-2:]
@ -104,167 +108,285 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string)
processElement = profileSet.UserProfile().Name processElement = profileSet.UserProfile().Name
break matchLoop break matchLoop
} }
if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 { if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 {
matched = true matched = true
processElement = connection.Process().Name processElement = comm.Process().Name
break matchLoop break matchLoop
} }
if levenshtein.Match(domainElement, connection.Process().ExecName, nil) > 0.5 { if levenshtein.Match(domainElement, comm.Process().ExecName, nil) > 0.5 {
matched = true matched = true
processElement = connection.Process().ExecName processElement = comm.Process().ExecName
break matchLoop break matchLoop
} }
} }
if matched { if matched {
log.Infof("firewall: accepting connection %s, match to domain was found: %s ~== %s", connection, domainElement, processElement) log.Infof("firewall: permitting communication %s, match to domain was found: %s ~== %s", comm, domainElement, processElement)
connection.Accept("domain is related to process") comm.Accept("domain is related to process")
} }
} }
if connection.GetVerdict() != network.ACCEPT { if comm.GetVerdict() != network.VerdictAccept {
// TODO // TODO
log.Infof("firewall: accepting connection %s, domain permitted (prompting is not yet implemented)", connection) log.Infof("firewall: permitting communication %s, domain permitted (prompting is not yet implemented)", comm)
connection.Accept("domain permitted (prompting is not yet implemented)") comm.Accept("domain permitted (prompting is not yet implemented)")
} }
return
case profile.Blacklist: case profile.Blacklist:
log.Infof("firewall: accepting connection %s, domain is not blacklisted", connection) log.Infof("firewall: permitting communication %s, domain is not blacklisted", comm)
connection.Accept("domain is not blacklisted") comm.Accept("domain is not blacklisted")
return
} }
log.Infof("firewall: denying communication %s, no profile mode set", comm)
comm.Deny("no profile mode set")
} }
// DecideOnConnectionAfterIntel makes a decision about a connection after the dns query is resolved and intel is gathered. // DecideOnCommunicationAfterIntel makes a decision about a communication after the dns query is resolved and intel is gathered.
func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache { func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, rrCache *intel.RRCache) {
// grant self // SUSPENDED until Stamp integration is finished
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection) // grant self - should not get here
connection.Accept("") // if comm.Process().Pid == os.Getpid() {
// log.Infof("firewall: granting own communication %s", comm)
// comm.Accept("")
// 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
return
}
// FilterDNSResponse filters a dns response according to the application profile and settings.
func FilterDNSResponse(comm *network.Communication, fqdn string, rrCache *intel.RRCache) *intel.RRCache {
// do not modify own queries - this should not happen anyway
if comm.Process().Pid == os.Getpid() {
return rrCache return rrCache
} }
// check if there is a profile // check if there is a profile
profileSet := connection.Process().ProfileSet() profileSet := comm.Process().ProfileSet()
if profileSet == nil { if profileSet == nil {
log.Errorf("firewall: denying connection %s, no Profile Set", connection) log.Infof("firewall: blocking dns query of communication %s, no Profile Set", comm)
connection.Deny("no Profile Set") return nil
return rrCache
} }
profileSet.Update(status.ActiveSecurityLevel()) profileSet.Update(status.ActiveSecurityLevel())
// TODO: Stamp integration // save config for consistency during function call
secLevel := profileSet.SecurityLevel()
filterByScope := filterDNSByScope(secLevel)
filterByProfile := filterDNSByProfile(secLevel)
// check if DNS response filtering is completely turned off
if !filterByScope && !filterByProfile {
return rrCache
}
// duplicate entry
rrCache = rrCache.ShallowCopy()
rrCache.FilteredEntries = make([]string, 0)
// change information
var addressesRemoved int
var addressesOk int
// loop vars
var classification int8
var ip net.IP
var result profile.EPResult
// filter function
filterEntries := func(entries []dns.RR) (goodEntries []dns.RR) {
goodEntries = make([]dns.RR, 0, len(entries))
for _, rr := range entries {
// get IP and classification
switch v := rr.(type) {
case *dns.A:
ip = v.A
case *dns.AAAA:
ip = v.AAAA
default:
// add non A/AAAA entries
goodEntries = append(goodEntries, rr)
continue
}
classification = netutils.ClassifyIP(ip)
if filterByScope {
switch {
case classification == netutils.HostLocal:
// No DNS should return localhost addresses
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
case rrCache.ServerScope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
// No global DNS should return LAN addresses
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
}
}
if filterByProfile {
// filter by flags
switch {
case !profileSet.CheckFlag(profile.Internet) && classification == netutils.Global:
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
case !profileSet.CheckFlag(profile.LAN) && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
case !profileSet.CheckFlag(profile.Localhost) && classification == netutils.HostLocal:
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
}
// filter by endpoints
result, _ = profileSet.CheckEndpointIP("", ip, 0, 0, false)
if result == profile.Denied {
addressesRemoved++
rrCache.FilteredEntries = append(rrCache.FilteredEntries, rr.String())
continue
}
}
// if survived, add to good entries
addressesOk++
goodEntries = append(goodEntries, rr)
}
return
}
rrCache.Answer = filterEntries(rrCache.Answer)
rrCache.Extra = filterEntries(rrCache.Extra)
if addressesRemoved > 0 {
rrCache.Filtered = true
if addressesOk == 0 {
comm.Deny("no addresses returned for this domain are permitted")
log.Infof("firewall: fully dns responses for communication %s", comm)
return nil
}
}
if rrCache.Filtered {
log.Infof("firewall: filtered DNS replies for %s: %s", comm, strings.Join(rrCache.FilteredEntries, ", "))
}
// TODO: Gate17 integration // TODO: Gate17 integration
// tunnelInfo, err := AssignTunnelIP(fqdn) // tunnelInfo, err := AssignTunnelIP(fqdn)
rrCache.Duplicate().FilterEntries(profileSet.CheckFlag(profile.Internet), profileSet.CheckFlag(profile.LAN), false)
if len(rrCache.Answer) == 0 {
if profileSet.CheckFlag(profile.Internet) {
connection.Deny("server is located in the LAN, but LAN access is not permitted")
} else {
connection.Deny("server is located in the Internet, but Internet access is not permitted")
}
}
return rrCache return rrCache
} }
// DeciceOnConnection makes a decision about a connection with its first packet. // DecideOnCommunication makes a decision about a communication with its first packet.
func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
// grant self // grant self
if connection.Process().Pid == os.Getpid() { if comm.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection) log.Infof("firewall: granting own communication %s", comm)
connection.Accept("") comm.Accept("")
return return
} }
// check if there is a profile // check if there is a profile
profileSet := connection.Process().ProfileSet() profileSet := comm.Process().ProfileSet()
if profileSet == nil { if profileSet == nil {
log.Errorf("firewall: denying connection %s, no Profile Set", connection) log.Errorf("firewall: denying communication %s, no Profile Set", comm)
connection.Deny("no Profile Set") comm.Deny("no Profile Set")
return return
} }
profileSet.Update(status.ActiveSecurityLevel()) profileSet.Update(status.ActiveSecurityLevel())
// check connection type // check comm type
switch connection.Domain { switch comm.Domain {
case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid: case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
if !profileSet.CheckFlag(profile.Service) { if !profileSet.CheckFlag(profile.Service) {
log.Infof("firewall: denying connection %s, not a service", connection) log.Infof("firewall: denying communication %s, not a service", comm)
if connection.Domain == network.IncomingHost { if comm.Domain == network.IncomingHost {
connection.Block("not a service") comm.Block("not a service")
} else { } else {
connection.Drop("not a service") comm.Drop("not a service")
} }
return return
} }
case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing! case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing!
if !profileSet.CheckFlag(profile.PeerToPeer) { if !profileSet.CheckFlag(profile.PeerToPeer) {
log.Infof("firewall: denying connection %s, peer to peer connections (to an IP) not allowed", connection) log.Infof("firewall: denying communication %s, peer to peer comms (to an IP) not allowed", comm)
connection.Deny("peer to peer connections (to an IP) not allowed") comm.Deny("peer to peer comms (to an IP) not allowed")
return return
} }
default:
} }
// check network scope // check network scope
switch connection.Domain { switch comm.Domain {
case network.IncomingHost: case network.IncomingHost:
if !profileSet.CheckFlag(profile.Localhost) { if !profileSet.CheckFlag(profile.Localhost) {
log.Infof("firewall: denying connection %s, serving localhost not allowed", connection) log.Infof("firewall: denying communication %s, serving localhost not allowed", comm)
connection.Block("serving localhost not allowed") comm.Block("serving localhost not allowed")
return return
} }
case network.IncomingLAN: case network.IncomingLAN:
if !profileSet.CheckFlag(profile.LAN) { if !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, serving LAN not allowed", connection) log.Infof("firewall: denying communication %s, serving LAN not allowed", comm)
connection.Deny("serving LAN not allowed") comm.Deny("serving LAN not allowed")
return return
} }
case network.IncomingInternet: case network.IncomingInternet:
if !profileSet.CheckFlag(profile.Internet) { if !profileSet.CheckFlag(profile.Internet) {
log.Infof("firewall: denying connection %s, serving Internet not allowed", connection) log.Infof("firewall: denying communication %s, serving Internet not allowed", comm)
connection.Deny("serving Internet not allowed") comm.Deny("serving Internet not allowed")
return return
} }
case network.IncomingInvalid: case network.IncomingInvalid:
log.Infof("firewall: denying connection %s, invalid IP address", connection) log.Infof("firewall: denying communication %s, invalid IP address", comm)
connection.Drop("invalid IP address") comm.Drop("invalid IP address")
return return
case network.PeerHost: case network.PeerHost:
if !profileSet.CheckFlag(profile.Localhost) { if !profileSet.CheckFlag(profile.Localhost) {
log.Infof("firewall: denying connection %s, accessing localhost not allowed", connection) log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm)
connection.Block("accessing localhost not allowed") comm.Block("accessing localhost not allowed")
return return
} }
case network.PeerLAN: case network.PeerLAN:
if !profileSet.CheckFlag(profile.LAN) { if !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, accessing the LAN not allowed", connection) log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm)
connection.Deny("accessing the LAN not allowed") comm.Deny("accessing the LAN not allowed")
return return
} }
case network.PeerInternet: case network.PeerInternet:
if !profileSet.CheckFlag(profile.Internet) { if !profileSet.CheckFlag(profile.Internet) {
log.Infof("firewall: denying connection %s, accessing the Internet not allowed", connection) log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm)
connection.Deny("accessing the Internet not allowed") comm.Deny("accessing the Internet not allowed")
return return
} }
case network.PeerInvalid: case network.PeerInvalid:
log.Infof("firewall: denying connection %s, invalid IP address", connection) log.Infof("firewall: denying communication %s, invalid IP address", comm)
connection.Deny("invalid IP address") comm.Deny("invalid IP address")
return return
} }
log.Infof("firewall: accepting connection %s", connection) log.Infof("firewall: undeterminable verdict for communication %s", comm)
connection.Accept("")
} }
// DecideOnLink makes a decision about a link with the first packet. // DecideOnLink makes a decision about a link with the first packet.
func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet.Packet) { func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Packet) {
// check: // check:
// Profile.Flags // Profile.Flags
// - network specific: Internet, LocalNet // - network specific: Internet, LocalNet
@ -272,34 +394,37 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
// Profile.ListenPorts // Profile.ListenPorts
// grant self // grant self
if connection.Process().Pid == os.Getpid() { if comm.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own link %s", connection) log.Infof("firewall: granting own link %s", comm)
connection.Accept("") link.Accept("")
return return
} }
// check if there is a profile // check if there is a profile
profileSet := connection.Process().ProfileSet() profileSet := comm.Process().ProfileSet()
if profileSet == nil { if profileSet == nil {
log.Infof("firewall: no Profile Set, denying %s", link) log.Infof("firewall: no Profile Set, denying %s", link)
link.Block("no Profile Set") link.Deny("no Profile Set")
return return
} }
profileSet.Update(status.ActiveSecurityLevel()) profileSet.Update(status.ActiveSecurityLevel())
// get host // get domain
var domainOrIP string var domain string
switch { if strings.HasSuffix(comm.Domain, ".") {
case strings.HasSuffix(connection.Domain, "."): domain = comm.Domain
domainOrIP = connection.Domain
case connection.Direction:
domainOrIP = pkt.GetIPHeader().Src.String()
default:
domainOrIP = pkt.GetIPHeader().Dst.String()
} }
// get protocol / destination port // remoteIP
protocol := pkt.GetIPHeader().Protocol var remoteIP net.IP
if comm.Direction {
remoteIP = pkt.GetIPHeader().Src
} else {
remoteIP = pkt.GetIPHeader().Dst
}
// protocol and destination port
protocol := uint8(pkt.GetIPHeader().Protocol)
var dstPort uint16 var dstPort uint16
tcpUDPHeader := pkt.GetTCPUDPHeader() tcpUDPHeader := pkt.GetTCPUDPHeader()
if tcpUDPHeader != nil { if tcpUDPHeader != nil {
@ -307,15 +432,17 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
} }
// check endpoints list // check endpoints list
permitted, reason, ok := profileSet.CheckEndpoint(domainOrIP, uint8(protocol), dstPort, connection.Direction) result, reason := profileSet.CheckEndpointIP(domain, remoteIP, protocol, dstPort, comm.Direction)
if ok { switch result {
if permitted { // case profile.NoMatch, profile.Undeterminable:
log.Infof("firewall: accepting link %s, endpoint is whitelisted: %s", link, reason) // continue
link.Accept(fmt.Sprintf("port whitelisted: %s", reason)) case profile.Denied:
} else { log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason)
log.Infof("firewall: denying link %s: endpoint is blacklisted: %s", link, reason) link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
link.Deny("port blacklisted") return
} case profile.Permitted:
log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason)
link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
return return
} }
@ -325,15 +452,15 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
link.Deny("endpoint is not whitelisted") link.Deny("endpoint is not whitelisted")
return return
case profile.Prompt: case profile.Prompt:
log.Infof("firewall: accepting link %s: endpoint is not blacklisted (prompting is not yet implemented)", link) log.Infof("firewall: permitting link %s: endpoint is not blacklisted (prompting is not yet implemented)", link)
link.Accept("endpoint is not blacklisted (prompting is not yet implemented)") link.Accept("endpoint is not blacklisted (prompting is not yet implemented)")
return return
case profile.Blacklist: case profile.Blacklist:
log.Infof("firewall: accepting link %s: endpoint is not blacklisted", link) log.Infof("firewall: permitting link %s: endpoint is not blacklisted", link)
link.Accept("endpoint is not blacklisted") link.Accept("endpoint is not blacklisted")
return return
} }
log.Infof("firewall: accepting link %s", link) log.Infof("firewall: denying link %s, no profile mode set", link)
link.Accept("") link.Deny("no profile mode set")
} }

View file

@ -58,8 +58,8 @@ func prep() error {
ExpertiseLevel: config.ExpertiseLevelExpert, ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 3, DefaultValue: 7,
ValidationRegex: "^(1|2|3)$", ValidationRegex: "^(7|6|4)$",
}) })
if err != nil { if err != nil {
return err return err
@ -73,8 +73,8 @@ func prep() error {
ExpertiseLevel: config.ExpertiseLevelExpert, ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 3, DefaultValue: 7,
ValidationRegex: "^(1|2|3)$", ValidationRegex: "^(7|6|4)$",
}) })
if err != nil { if err != nil {
return err return err
@ -88,8 +88,8 @@ func prep() error {
ExpertiseLevel: config.ExpertiseLevelExpert, ExpertiseLevel: config.ExpertiseLevelExpert,
OptType: config.OptTypeInt, OptType: config.OptTypeInt,
ExternalOptType: "security level", ExternalOptType: "security level",
DefaultValue: 3, DefaultValue: 7,
ValidationRegex: "^(1|2|3)$", ValidationRegex: "^(7|6|4)$",
}) })
if err != nil { if err != nil {
return err return err

View file

@ -7,11 +7,11 @@ import (
"github.com/Safing/portbase/modules" "github.com/Safing/portbase/modules"
// module dependencies // module dependencies
_ "github.com/Safing/portmaster/global" _ "github.com/Safing/portmaster/core"
) )
func init() { func init() {
modules.Register("intel", prep, start, nil, "global") modules.Register("intel", prep, start, nil, "core")
} }
func start() error { func start() error {

View file

@ -27,7 +27,9 @@ type NameRecord struct {
Ns []string Ns []string
Extra []string Extra []string
TTL int64 TTL int64
Filtered bool
Server string
ServerScope int8
} }
func makeNameRecordKey(domain string, question string) string { func makeNameRecordKey(domain string, question string) string {

View file

@ -15,7 +15,6 @@ import (
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/status" "github.com/Safing/portmaster/status"
) )
@ -304,13 +303,6 @@ func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype
} }
resolver.Initialized.SetToIf(false, true) resolver.Initialized.SetToIf(false, true)
// remove localhost entries, remove LAN entries if server is in global IP space.
if resolver.ServerIPScope == netutils.Global {
rrCache.FilterEntries(true, false, false)
} else {
rrCache.FilterEntries(true, true, false)
}
return rrCache, true return rrCache, true
} }
@ -357,11 +349,13 @@ func query(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
} }
new := &RRCache{ new := &RRCache{
Domain: fqdn, Domain: fqdn,
Question: qtype, Question: qtype,
Answer: reply.Answer, Answer: reply.Answer,
Ns: reply.Ns, Ns: reply.Ns,
Extra: reply.Extra, Extra: reply.Extra,
Server: resolver.Server,
ServerScope: resolver.ServerIPScope,
} }
// TODO: check if reply.Answer is valid // TODO: check if reply.Answer is valid

View file

@ -5,11 +5,8 @@ package intel
import ( import (
"fmt" "fmt"
"net" "net"
"strings"
"time" "time"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -23,10 +20,14 @@ type RRCache struct {
Extra []dns.RR Extra []dns.RR
TTL int64 TTL int64
Server string
ServerScope int8
updated int64 updated int64
servedFromCache bool servedFromCache bool
requestingNew bool requestingNew bool
Filtered bool Filtered bool
FilteredEntries []string
} }
// Clean sets all TTLs to 17 and sets cache expiry with specified minimum. // Clean sets all TTLs to 17 and sets cache expiry with specified minimum.
@ -79,10 +80,11 @@ func (m *RRCache) ExportAllARecords() (ips []net.IP) {
// ToNameRecord converts the RRCache to a NameRecord for cleaner persistence. // ToNameRecord converts the RRCache to a NameRecord for cleaner persistence.
func (m *RRCache) ToNameRecord() *NameRecord { func (m *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{ new := &NameRecord{
Domain: m.Domain, Domain: m.Domain,
Question: m.Question.String(), Question: m.Question.String(),
TTL: m.TTL, TTL: m.TTL,
Filtered: m.Filtered, Server: m.Server,
ServerScope: m.ServerScope,
} }
// stringify RR entries // stringify RR entries
@ -136,7 +138,8 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
} }
} }
rrCache.Filtered = nameRecord.Filtered rrCache.Server = nameRecord.Server
rrCache.ServerScope = nameRecord.ServerScope
rrCache.servedFromCache = true rrCache.servedFromCache = true
return rrCache, nil return rrCache, nil
} }
@ -175,82 +178,23 @@ func (m *RRCache) IsNXDomain() bool {
return len(m.Answer) == 0 return len(m.Answer) == 0
} }
// Duplicate returns a duplicate of the cache. slices are not copied, but referenced. // ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced.
func (m *RRCache) Duplicate() *RRCache { func (m *RRCache) ShallowCopy() *RRCache {
return &RRCache{ return &RRCache{
Domain: m.Domain, Domain: m.Domain,
Question: m.Question, Question: m.Question,
Answer: m.Answer, Answer: m.Answer,
Ns: m.Ns, Ns: m.Ns,
Extra: m.Extra, Extra: m.Extra,
TTL: m.TTL, TTL: m.TTL,
Server: m.Server,
ServerScope: m.ServerScope,
updated: m.updated, updated: m.updated,
servedFromCache: m.servedFromCache, servedFromCache: m.servedFromCache,
requestingNew: m.requestingNew, requestingNew: m.requestingNew,
Filtered: m.Filtered, Filtered: m.Filtered,
FilteredEntries: m.FilteredEntries,
} }
} }
// FilterEntries filters resource records according to the given permission scope.
func (m *RRCache) FilterEntries(internet, lan, host bool) {
var filtered bool
m.Answer, filtered = filterEntries(m, m.Answer, internet, lan, host)
if filtered {
m.Filtered = true
}
m.Extra, filtered = filterEntries(m, m.Extra, internet, lan, host)
if filtered {
m.Filtered = true
}
}
func filterEntries(m *RRCache, entries []dns.RR, internet, lan, host bool) (filteredEntries []dns.RR, filtered bool) {
filteredEntries = make([]dns.RR, 0, len(entries))
var classification int8
var deletedEntries []string
entryLoop:
for _, rr := range entries {
classification = -1
switch v := rr.(type) {
case *dns.A:
classification = netutils.ClassifyIP(v.A)
case *dns.AAAA:
classification = netutils.ClassifyIP(v.AAAA)
}
if classification >= 0 {
switch {
case !internet && classification == netutils.Global:
filtered = true
deletedEntries = append(deletedEntries, rr.String())
continue entryLoop
case !lan && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
filtered = true
deletedEntries = append(deletedEntries, rr.String())
continue entryLoop
case !host && classification == netutils.HostLocal:
filtered = true
deletedEntries = append(deletedEntries, rr.String())
continue entryLoop
}
}
filteredEntries = append(filteredEntries, rr)
}
if len(deletedEntries) > 0 {
log.Infof("intel: filtered DNS replies for %s%s: %s (Settings: Int=%v LAN=%v Host=%v)",
m.Domain,
m.Question.String(),
strings.Join(deletedEntries, ", "),
internet,
lan,
host,
)
}
return
}

View file

@ -14,10 +14,7 @@ import (
"github.com/Safing/portbase/modules" "github.com/Safing/portbase/modules"
// include packages here // include packages here
_ "github.com/Safing/portmaster/core"
_ "github.com/Safing/portbase/api"
_ "github.com/Safing/portbase/database/dbmodule"
_ "github.com/Safing/portbase/database/storage/badger"
_ "github.com/Safing/portmaster/firewall" _ "github.com/Safing/portmaster/firewall"
_ "github.com/Safing/portmaster/nameserver" _ "github.com/Safing/portmaster/nameserver"
_ "github.com/Safing/portmaster/ui" _ "github.com/Safing/portmaster/ui"

View file

@ -126,8 +126,8 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
// get connection // get connection
// start = time.Now() // start = time.Now()
connection, err := network.GetConnectionByDNSRequest(rAddr.IP, uint16(rAddr.Port), fqdn) comm, err := network.GetCommunicationByDNSRequest(rAddr.IP, uint16(rAddr.Port), fqdn)
// log.Tracef("nameserver: took %s to get connection (and maybe process)", time.Since(start)) // log.Tracef("nameserver: took %s to get comms (and maybe process)", time.Since(start))
if err != nil { if err != nil {
log.Warningf("nameserver: someone is requesting %s, but could not identify process: %s, returning nxdomain", fqdn, err) log.Warningf("nameserver: someone is requesting %s, but could not identify process: %s, returning nxdomain", fqdn, err)
nxDomain(w, query) nxDomain(w, query)
@ -137,39 +137,51 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
// [2/2] use this to time how long it takes to get process info // [2/2] use this to time how long it takes to get process info
// log.Tracef("nameserver: took %s to get connection/process of %s request", time.Now().Sub(timed).String(), fqdn) // log.Tracef("nameserver: took %s to get connection/process of %s request", time.Now().Sub(timed).String(), fqdn)
// check if communication needs reevaluation
if comm.NeedsReevaluation() {
comm.ResetVerdict()
}
// check profile before we even get intel and rr // check profile before we even get intel and rr
if connection.GetVerdict() == network.UNDECIDED { if comm.GetVerdict() == network.VerdictUndecided || comm.GetVerdict() == network.VerdictUndeterminable {
// start = time.Now() // start = time.Now()
firewall.DecideOnConnectionBeforeIntel(connection, fqdn) firewall.DecideOnCommunicationBeforeIntel(comm, fqdn)
// log.Tracef("nameserver: took %s to make decision", time.Since(start)) // log.Tracef("nameserver: took %s to make decision", time.Since(start))
} }
if connection.GetVerdict() == network.BLOCK || connection.GetVerdict() == network.DROP { if comm.GetVerdict() == network.VerdictBlock || comm.GetVerdict() == network.VerdictDrop {
nxDomain(w, query) nxDomain(w, query)
return return
} }
// get intel and RRs // get intel and RRs
// start = time.Now() // start = time.Now()
domainIntel, rrCache := intel.GetIntelAndRRs(fqdn, qtype, connection.Process().ProfileSet().SecurityLevel()) domainIntel, rrCache := intel.GetIntelAndRRs(fqdn, qtype, comm.Process().ProfileSet().SecurityLevel())
// log.Tracef("nameserver: took %s to get intel and RRs", time.Since(start)) // log.Tracef("nameserver: took %s to get intel and RRs", time.Since(start))
if rrCache == nil { if rrCache == nil {
// TODO: analyze nxdomain requests, malware could be trying DGA-domains // TODO: analyze nxdomain requests, malware could be trying DGA-domains
log.Infof("nameserver: %s tried to query %s, but is nxdomain", connection.Process().String(), fqdn) log.Infof("nameserver: %s tried to query %s, but is nxdomain", comm.Process().String(), fqdn)
nxDomain(w, query) nxDomain(w, query)
return return
} }
// set intel // set intel
connection.Lock() comm.Lock()
connection.Intel = domainIntel comm.Intel = domainIntel
connection.Unlock() comm.Unlock()
connection.Save() comm.Save()
// do a full check with intel // do a full check with intel
if connection.GetVerdict() == network.UNDECIDED { if comm.GetVerdict() == network.VerdictUndecided || comm.GetVerdict() == network.VerdictUndeterminable {
rrCache = firewall.DecideOnConnectionAfterIntel(connection, fqdn, rrCache) firewall.DecideOnCommunicationAfterIntel(comm, fqdn, rrCache)
} }
if rrCache == nil || connection.GetVerdict() == network.BLOCK || connection.GetVerdict() == network.DROP { if comm.GetVerdict() == network.VerdictBlock || comm.GetVerdict() == network.VerdictDrop {
nxDomain(w, query)
return
}
// filter DNS response
rrCache = firewall.FilterDNSResponse(comm, fqdn, rrCache)
if rrCache == nil {
nxDomain(w, query) nxDomain(w, query)
return return
} }

View file

@ -20,7 +20,7 @@ func cleaner() {
cleanLinks() cleanLinks()
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
cleanConnections() cleanComms()
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
cleanProcesses() cleanProcesses()
} }
@ -73,18 +73,18 @@ func cleanLinks() {
} }
} }
func cleanConnections() { func cleanComms() {
connectionsLock.RLock() commsLock.RLock()
defer connectionsLock.RUnlock() defer commsLock.RUnlock()
threshold := time.Now().Add(-thresholdDuration).Unix() threshold := time.Now().Add(-thresholdDuration).Unix()
for _, conn := range connections { for _, comm := range comms {
conn.Lock() comm.Lock()
if conn.FirstLinkEstablished < threshold && conn.LinkCount == 0 { if comm.FirstLinkEstablished < threshold && comm.LinkCount == 0 {
// log.Tracef("network.clean: deleted %s", conn.DatabaseKey()) // log.Tracef("network.clean: deleted %s", comm.DatabaseKey())
go conn.Delete() go comm.Delete()
} }
conn.Unlock() comm.Unlock()
} }
} }

349
network/communication.go Normal file
View file

@ -0,0 +1,349 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package network
import (
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/Safing/portbase/database/record"
"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.
type Communication struct {
record.Base
sync.Mutex
Domain string
Direction bool
Intel *intel.Intel
process *process.Process
Verdict Verdict
Reason string
Inspect bool
FirstLinkEstablished int64
LastLinkEstablished int64
LinkCount uint
profileUpdateVersion uint32
}
// Process returns the process that owns the connection.
func (comm *Communication) Process() *process.Process {
comm.Lock()
defer comm.Unlock()
return comm.process
}
// ResetVerdict resets the verdict to VerdictUndecided.
func (comm *Communication) ResetVerdict() {
comm.Lock()
defer comm.Unlock()
comm.Verdict = VerdictUndecided
}
// GetVerdict returns the current verdict.
func (comm *Communication) GetVerdict() Verdict {
comm.Lock()
defer comm.Unlock()
return comm.Verdict
}
// Accept accepts the communication and adds the given reason.
func (comm *Communication) Accept(reason string) {
comm.AddReason(reason)
comm.UpdateVerdict(VerdictAccept)
}
// Deny blocks or drops the communication depending on the connection direction and adds the given reason.
func (comm *Communication) Deny(reason string) {
if comm.Direction {
comm.Drop(reason)
} else {
comm.Block(reason)
}
}
// Block blocks the communication and adds the given reason.
func (comm *Communication) Block(reason string) {
comm.AddReason(reason)
comm.UpdateVerdict(VerdictBlock)
}
// Drop drops the communication and adds the given reason.
func (comm *Communication) Drop(reason string) {
comm.AddReason(reason)
comm.UpdateVerdict(VerdictDrop)
}
// 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()
if newVerdict > comm.Verdict {
comm.Verdict = newVerdict
go comm.Save()
}
}
// AddReason adds a human readable string as to why a certain verdict was set in regard to this communication.
func (comm *Communication) AddReason(reason string) {
if reason == "" {
return
}
comm.Lock()
defer comm.Unlock()
if comm.Reason != "" {
comm.Reason += " | "
}
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()
updateVersion := profile.GetUpdateVersion()
if comm.profileUpdateVersion != updateVersion {
comm.profileUpdateVersion = updateVersion
return true
}
return false
}
// GetCommunicationByFirstPacket returns the matching communication from the internal storage.
func GetCommunicationByFirstPacket(pkt packet.Packet) (*Communication, error) {
// get Process
proc, direction, err := process.GetProcessByPacket(pkt)
if err != nil {
return nil, err
}
var domain string
// Incoming
if direction {
switch netutils.ClassifyIP(pkt.GetIPHeader().Src) {
case netutils.HostLocal:
domain = IncomingHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = IncomingLAN
case netutils.Global, netutils.GlobalMulticast:
domain = IncomingInternet
case netutils.Invalid:
domain = IncomingInvalid
}
communication, ok := GetCommunication(proc.Pid, domain)
if !ok {
communication = &Communication{
Domain: domain,
Direction: Inbound,
process: proc,
Inspect: true,
FirstLinkEstablished: time.Now().Unix(),
}
}
communication.process.AddCommunication()
return communication, nil
}
// get domain
ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP())
// PeerToPeer
if err != nil {
// if no domain could be found, it must be a direct connection (ie. no DNS)
switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) {
case netutils.HostLocal:
domain = PeerHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = PeerLAN
case netutils.Global, netutils.GlobalMulticast:
domain = PeerInternet
case netutils.Invalid:
domain = PeerInvalid
}
communication, ok := GetCommunication(proc.Pid, domain)
if !ok {
communication = &Communication{
Domain: domain,
Direction: Outbound,
process: proc,
Inspect: true,
FirstLinkEstablished: time.Now().Unix(),
}
}
communication.process.AddCommunication()
return communication, nil
}
// To Domain
// FIXME: how to handle multiple possible domains?
communication, ok := GetCommunication(proc.Pid, ipinfo.Domains[0])
if !ok {
communication = &Communication{
Domain: ipinfo.Domains[0],
Direction: Outbound,
process: proc,
Inspect: true,
FirstLinkEstablished: time.Now().Unix(),
}
}
communication.process.AddCommunication()
return communication, nil
}
// var localhost = net.IPv4(127, 0, 0, 1)
var (
dnsAddress = net.IPv4(127, 0, 0, 1)
dnsPort uint16 = 53
)
// GetCommunicationByDNSRequest returns the matching communication from the internal storage.
func GetCommunicationByDNSRequest(ip net.IP, port uint16, fqdn string) (*Communication, error) {
// get Process
proc, err := process.GetProcessByEndpoints(ip, port, dnsAddress, dnsPort, packet.UDP)
if err != nil {
return nil, err
}
communication, ok := GetCommunication(proc.Pid, fqdn)
if !ok {
communication = &Communication{
Domain: fqdn,
process: proc,
Inspect: true,
}
communication.process.AddCommunication()
communication.Save()
}
return communication, nil
}
// GetCommunication fetches a connection object from the internal storage.
func GetCommunication(pid int, domain string) (comm *Communication, ok bool) {
commsLock.RLock()
defer commsLock.RUnlock()
comm, ok = comms[fmt.Sprintf("%d/%s", pid, domain)]
return
}
func (comm *Communication) makeKey() string {
return fmt.Sprintf("%d/%s", comm.process.Pid, comm.Domain)
}
// Save saves the connection object in the storage and propagates the change.
func (comm *Communication) Save() error {
comm.Lock()
defer comm.Unlock()
if comm.process == nil {
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.CreateMeta()
}
key := comm.makeKey()
commsLock.RLock()
_, ok := comms[key]
commsLock.RUnlock()
if !ok {
commsLock.Lock()
comms[key] = comm
commsLock.Unlock()
}
go dbController.PushUpdate(comm)
return nil
}
// Delete deletes a connection from the storage and propagates the change.
func (comm *Communication) Delete() {
comm.Lock()
defer comm.Unlock()
commsLock.Lock()
delete(comms, comm.makeKey())
commsLock.Unlock()
comm.Meta().Delete()
go dbController.PushUpdate(comm)
comm.process.RemoveCommunication()
go comm.process.Save()
}
// AddLink applies the Communication to the Link and increases sets counter and timestamps.
func (comm *Communication) AddLink(link *Link) {
link.Lock()
link.comm = comm
link.Verdict = comm.Verdict
link.Inspect = comm.Inspect
link.Unlock()
link.Save()
comm.Lock()
comm.LinkCount++
comm.LastLinkEstablished = time.Now().Unix()
if comm.FirstLinkEstablished == 0 {
comm.FirstLinkEstablished = comm.LastLinkEstablished
}
comm.Unlock()
comm.Save()
}
// RemoveLink lowers the link counter by one.
func (comm *Communication) RemoveLink() {
comm.Lock()
defer comm.Unlock()
if comm.LinkCount > 0 {
comm.LinkCount--
}
}
// String returns a string representation of Communication.
func (comm *Communication) String() string {
comm.Lock()
defer comm.Unlock()
switch comm.Domain {
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if comm.process == nil {
return "? <- *"
}
return fmt.Sprintf("%s <- *", comm.process.String())
case PeerHost, PeerLAN, PeerInternet, PeerInvalid:
if comm.process == nil {
return "? -> *"
}
return fmt.Sprintf("%s -> *", comm.process.String())
default:
if comm.process == nil {
return fmt.Sprintf("? -> %s", comm.Domain)
}
return fmt.Sprintf("%s -> %s", comm.process.String(), comm.Domain)
}
}

View file

@ -1,325 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package network
import (
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/Safing/portbase/database/record"
"github.com/Safing/portmaster/intel"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/network/packet"
"github.com/Safing/portmaster/process"
)
// Connection describes a connection between a process and a domain
type Connection struct {
record.Base
sync.Mutex
Domain string
Direction bool
Intel *intel.Intel
process *process.Process
Verdict Verdict
Reason string
Inspect bool
FirstLinkEstablished int64
LastLinkEstablished int64
LinkCount uint
}
// Process returns the process that owns the connection.
func (conn *Connection) Process() *process.Process {
conn.Lock()
defer conn.Unlock()
return conn.process
}
// GetVerdict returns the current verdict.
func (conn *Connection) GetVerdict() Verdict {
conn.Lock()
defer conn.Unlock()
return conn.Verdict
}
// Accept accepts the connection and adds the given reason.
func (conn *Connection) Accept(reason string) {
conn.AddReason(reason)
conn.UpdateVerdict(ACCEPT)
}
// Deny blocks or drops the connection depending on the connection direction and adds the given reason.
func (conn *Connection) Deny(reason string) {
if conn.Direction {
conn.Drop(reason)
} else {
conn.Block(reason)
}
}
// Block blocks the connection and adds the given reason.
func (conn *Connection) Block(reason string) {
conn.AddReason(reason)
conn.UpdateVerdict(BLOCK)
}
// Drop drops the connection and adds the given reason.
func (conn *Connection) Drop(reason string) {
conn.AddReason(reason)
conn.UpdateVerdict(DROP)
}
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
func (conn *Connection) UpdateVerdict(newVerdict Verdict) {
conn.Lock()
defer conn.Unlock()
if newVerdict > conn.Verdict {
conn.Verdict = newVerdict
go conn.Save()
}
}
// AddReason adds a human readable string as to why a certain verdict was set in regard to this connection
func (conn *Connection) AddReason(reason string) {
if reason == "" {
return
}
conn.Lock()
defer conn.Unlock()
if conn.Reason != "" {
conn.Reason += " | "
}
conn.Reason += reason
}
// GetConnectionByFirstPacket returns the matching connection from the internal storage.
func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) {
// get Process
proc, direction, err := process.GetProcessByPacket(pkt)
if err != nil {
return nil, err
}
var domain string
// Incoming
if direction {
switch netutils.ClassifyIP(pkt.GetIPHeader().Src) {
case netutils.HostLocal:
domain = IncomingHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = IncomingLAN
case netutils.Global, netutils.GlobalMulticast:
domain = IncomingInternet
case netutils.Invalid:
domain = IncomingInvalid
}
connection, ok := GetConnection(proc.Pid, domain)
if !ok {
connection = &Connection{
Domain: domain,
Direction: Inbound,
process: proc,
Inspect: true,
FirstLinkEstablished: time.Now().Unix(),
}
}
connection.process.AddConnection()
return connection, nil
}
// get domain
ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP())
// PeerToPeer
if err != nil {
// if no domain could be found, it must be a direct connection
switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) {
case netutils.HostLocal:
domain = PeerHost
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
domain = PeerLAN
case netutils.Global, netutils.GlobalMulticast:
domain = PeerInternet
case netutils.Invalid:
domain = PeerInvalid
}
connection, ok := GetConnection(proc.Pid, domain)
if !ok {
connection = &Connection{
Domain: domain,
Direction: Outbound,
process: proc,
Inspect: true,
FirstLinkEstablished: time.Now().Unix(),
}
}
connection.process.AddConnection()
return connection, nil
}
// To Domain
// FIXME: how to handle multiple possible domains?
connection, ok := GetConnection(proc.Pid, ipinfo.Domains[0])
if !ok {
connection = &Connection{
Domain: ipinfo.Domains[0],
Direction: Outbound,
process: proc,
Inspect: true,
FirstLinkEstablished: time.Now().Unix(),
}
}
connection.process.AddConnection()
return connection, nil
}
// var localhost = net.IPv4(127, 0, 0, 1)
var (
dnsAddress = net.IPv4(127, 0, 0, 1)
dnsPort uint16 = 53
)
// GetConnectionByDNSRequest returns the matching connection from the internal storage.
func GetConnectionByDNSRequest(ip net.IP, port uint16, fqdn string) (*Connection, error) {
// get Process
proc, err := process.GetProcessByEndpoints(ip, port, dnsAddress, dnsPort, packet.UDP)
if err != nil {
return nil, err
}
connection, ok := GetConnection(proc.Pid, fqdn)
if !ok {
connection = &Connection{
Domain: fqdn,
process: proc,
Inspect: true,
}
connection.process.AddConnection()
connection.Save()
}
return connection, nil
}
// GetConnection fetches a connection object from the internal storage.
func GetConnection(pid int, domain string) (conn *Connection, ok bool) {
connectionsLock.RLock()
defer connectionsLock.RUnlock()
conn, ok = connections[fmt.Sprintf("%d/%s", pid, domain)]
return
}
func (conn *Connection) makeKey() string {
return fmt.Sprintf("%d/%s", conn.process.Pid, conn.Domain)
}
// Save saves the connection object in the storage and propagates the change.
func (conn *Connection) Save() error {
conn.Lock()
defer conn.Unlock()
if conn.process == nil {
return errors.New("cannot save connection without process")
}
if !conn.KeyIsSet() {
conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Domain))
conn.CreateMeta()
}
key := conn.makeKey()
connectionsLock.RLock()
_, ok := connections[key]
connectionsLock.RUnlock()
if !ok {
connectionsLock.Lock()
connections[key] = conn
connectionsLock.Unlock()
}
go dbController.PushUpdate(conn)
return nil
}
// Delete deletes a connection from the storage and propagates the change.
func (conn *Connection) Delete() {
conn.Lock()
defer conn.Unlock()
connectionsLock.Lock()
delete(connections, conn.makeKey())
connectionsLock.Unlock()
conn.Meta().Delete()
go dbController.PushUpdate(conn)
conn.process.RemoveConnection()
go conn.process.Save()
}
// AddLink applies the connection to the link and increases sets counter and timestamps.
func (conn *Connection) AddLink(link *Link) {
link.Lock()
link.connection = conn
link.Verdict = conn.Verdict
link.Inspect = conn.Inspect
link.Unlock()
link.Save()
conn.Lock()
conn.LinkCount++
conn.LastLinkEstablished = time.Now().Unix()
if conn.FirstLinkEstablished == 0 {
conn.FirstLinkEstablished = conn.LastLinkEstablished
}
conn.Unlock()
conn.Save()
}
// RemoveLink lowers the link counter by one.
func (conn *Connection) RemoveLink() {
conn.Lock()
defer conn.Unlock()
if conn.LinkCount > 0 {
conn.LinkCount--
}
}
// String returns a string representation of Connection.
func (conn *Connection) String() string {
conn.Lock()
defer conn.Unlock()
switch conn.Domain {
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if conn.process == nil {
return "? <- *"
}
return fmt.Sprintf("%s <- *", conn.process.String())
case PeerHost, PeerLAN, PeerInternet, PeerInvalid:
if conn.process == nil {
return "? -> *"
}
return fmt.Sprintf("%s -> *", conn.process.String())
default:
if conn.process == nil {
return fmt.Sprintf("? -> %s", conn.Domain)
}
return fmt.Sprintf("%s -> %s", conn.process.String(), conn.Domain)
}
}

View file

@ -14,10 +14,10 @@ import (
) )
var ( var (
links = make(map[string]*Link) links = make(map[string]*Link)
linksLock sync.RWMutex linksLock sync.RWMutex
connections = make(map[string]*Connection) comms = make(map[string]*Communication)
connectionsLock sync.RWMutex commsLock sync.RWMutex
dbController *database.Controller dbController *database.Controller
) )
@ -43,9 +43,9 @@ func (s *StorageInterface) Get(key string) (record.Record, error) {
} }
} }
case 3: case 3:
connectionsLock.RLock() commsLock.RLock()
defer connectionsLock.RUnlock() defer commsLock.RUnlock()
conn, ok := connections[splitted[2]] conn, ok := comms[splitted[2]]
if ok { if ok {
return conn, nil return conn, nil
} }
@ -79,14 +79,14 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
} }
} }
// connections // comms
connectionsLock.RLock() commsLock.RLock()
for _, conn := range connections { for _, conn := range comms {
if strings.HasPrefix(conn.DatabaseKey(), q.DatabaseKeyPrefix()) { if strings.HasPrefix(conn.DatabaseKey(), q.DatabaseKeyPrefix()) {
it.Next <- conn it.Next <- conn
} }
} }
connectionsLock.RUnlock() commsLock.RUnlock()
// links // links
linksLock.RLock() linksLock.RLock()

View file

@ -38,18 +38,18 @@ type Link struct {
pktQueue chan packet.Packet pktQueue chan packet.Packet
firewallHandler FirewallHandler firewallHandler FirewallHandler
connection *Connection comm *Communication
activeInspectors []bool activeInspectors []bool
inspectorData map[uint8]interface{} inspectorData map[uint8]interface{}
} }
// Connection returns the Connection the Link is part of // Communication returns the Communication the Link is part of
func (link *Link) Connection() *Connection { func (link *Link) Communication() *Communication {
link.Lock() link.Lock()
defer link.Unlock() defer link.Unlock()
return link.connection return link.comm
} }
// GetVerdict returns the current verdict. // GetVerdict returns the current verdict.
@ -106,12 +106,12 @@ func (link *Link) HandlePacket(pkt packet.Packet) {
// Accept accepts the link and adds the given reason. // Accept accepts the link and adds the given reason.
func (link *Link) Accept(reason string) { func (link *Link) Accept(reason string) {
link.AddReason(reason) link.AddReason(reason)
link.UpdateVerdict(ACCEPT) link.UpdateVerdict(VerdictAccept)
} }
// Deny blocks or drops the link depending on the connection direction and adds the given reason. // Deny blocks or drops the link depending on the connection direction and adds the given reason.
func (link *Link) Deny(reason string) { func (link *Link) Deny(reason string) {
if link.connection != nil && link.connection.Direction { if link.comm != nil && link.comm.Direction {
link.Drop(reason) link.Drop(reason)
} else { } else {
link.Block(reason) link.Block(reason)
@ -121,24 +121,24 @@ func (link *Link) Deny(reason string) {
// Block blocks the link and adds the given reason. // Block blocks the link and adds the given reason.
func (link *Link) Block(reason string) { func (link *Link) Block(reason string) {
link.AddReason(reason) link.AddReason(reason)
link.UpdateVerdict(BLOCK) link.UpdateVerdict(VerdictBlock)
} }
// Drop drops the link and adds the given reason. // Drop drops the link and adds the given reason.
func (link *Link) Drop(reason string) { func (link *Link) Drop(reason string) {
link.AddReason(reason) link.AddReason(reason)
link.UpdateVerdict(DROP) link.UpdateVerdict(VerdictDrop)
} }
// RerouteToNameserver reroutes the link to the portmaster nameserver. // RerouteToNameserver reroutes the link to the portmaster nameserver.
func (link *Link) RerouteToNameserver() { func (link *Link) RerouteToNameserver() {
link.UpdateVerdict(RerouteToNameserver) link.UpdateVerdict(VerdictRerouteToNameserver)
} }
// RerouteToTunnel reroutes the link to the tunnel entrypoint and adds the given reason for accepting the connection. // RerouteToTunnel reroutes the link to the tunnel entrypoint and adds the given reason for accepting the connection.
func (link *Link) RerouteToTunnel(reason string) { func (link *Link) RerouteToTunnel(reason string) {
link.AddReason(reason) link.AddReason(reason)
link.UpdateVerdict(RerouteToTunnel) link.UpdateVerdict(VerdictRerouteToTunnel)
} }
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
@ -192,30 +192,30 @@ func (link *Link) ApplyVerdict(pkt packet.Packet) {
if link.VerdictPermanent { if link.VerdictPermanent {
switch link.Verdict { switch link.Verdict {
case ACCEPT: case VerdictAccept:
pkt.PermanentAccept() pkt.PermanentAccept()
case BLOCK: case VerdictBlock:
pkt.PermanentBlock() pkt.PermanentBlock()
case DROP: case VerdictDrop:
pkt.PermanentDrop() pkt.PermanentDrop()
case RerouteToNameserver: case VerdictRerouteToNameserver:
pkt.RerouteToNameserver() pkt.RerouteToNameserver()
case RerouteToTunnel: case VerdictRerouteToTunnel:
pkt.RerouteToTunnel() pkt.RerouteToTunnel()
default: default:
pkt.Drop() pkt.Drop()
} }
} else { } else {
switch link.Verdict { switch link.Verdict {
case ACCEPT: case VerdictAccept:
pkt.Accept() pkt.Accept()
case BLOCK: case VerdictBlock:
pkt.Block() pkt.Block()
case DROP: case VerdictDrop:
pkt.Drop() pkt.Drop()
case RerouteToNameserver: case VerdictRerouteToNameserver:
pkt.RerouteToNameserver() pkt.RerouteToNameserver()
case RerouteToTunnel: case VerdictRerouteToTunnel:
pkt.RerouteToTunnel() pkt.RerouteToTunnel()
default: default:
pkt.Drop() pkt.Drop()
@ -228,12 +228,12 @@ func (link *Link) Save() error {
link.Lock() link.Lock()
defer link.Unlock() defer link.Unlock()
if link.connection == nil { if link.comm == nil {
return errors.New("cannot save link without connection") return errors.New("cannot save link without comms")
} }
if !link.KeyIsSet() { if !link.KeyIsSet() {
link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.connection.Process().Pid, link.connection.Domain, link.ID)) link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.comm.Process().Pid, link.comm.Domain, link.ID))
link.CreateMeta() link.CreateMeta()
} }
@ -262,8 +262,8 @@ func (link *Link) Delete() {
link.Meta().Delete() link.Meta().Delete()
go dbController.PushUpdate(link) go dbController.PushUpdate(link)
link.connection.RemoveLink() link.comm.RemoveLink()
go link.connection.Save() go link.comm.Save()
} }
// GetLink fetches a Link from the database from the default namespace for this object // GetLink fetches a Link from the database from the default namespace for this object
@ -288,7 +288,7 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) {
func CreateLinkFromPacket(pkt packet.Packet) *Link { func CreateLinkFromPacket(pkt packet.Packet) *Link {
link := &Link{ link := &Link{
ID: pkt.GetLinkID(), ID: pkt.GetLinkID(),
Verdict: UNDECIDED, Verdict: VerdictUndecided,
Started: time.Now().Unix(), Started: time.Now().Unix(),
RemoteAddress: pkt.FmtRemoteAddress(), RemoteAddress: pkt.FmtRemoteAddress(),
} }
@ -332,24 +332,24 @@ func (link *Link) String() string {
link.Lock() link.Lock()
defer link.Unlock() defer link.Unlock()
if link.connection == nil { if link.comm == nil {
return fmt.Sprintf("? <-> %s", link.RemoteAddress) return fmt.Sprintf("? <-> %s", link.RemoteAddress)
} }
switch link.connection.Domain { switch link.comm.Domain {
case "I": case "I":
if link.connection.process == nil { if link.comm.process == nil {
return fmt.Sprintf("? <- %s", link.RemoteAddress) return fmt.Sprintf("? <- %s", link.RemoteAddress)
} }
return fmt.Sprintf("%s <- %s", link.connection.process.String(), link.RemoteAddress) return fmt.Sprintf("%s <- %s", link.comm.process.String(), link.RemoteAddress)
case "D": case "D":
if link.connection.process == nil { if link.comm.process == nil {
return fmt.Sprintf("? -> %s", link.RemoteAddress) return fmt.Sprintf("? -> %s", link.RemoteAddress)
} }
return fmt.Sprintf("%s -> %s", link.connection.process.String(), link.RemoteAddress) return fmt.Sprintf("%s -> %s", link.comm.process.String(), link.RemoteAddress)
default: default:
if link.connection.process == nil { if link.comm.process == nil {
return fmt.Sprintf("? -> %s (%s)", link.connection.Domain, link.RemoteAddress) return fmt.Sprintf("? -> %s (%s)", link.comm.Domain, link.RemoteAddress)
} }
return fmt.Sprintf("%s to %s (%s)", link.connection.process.String(), link.connection.Domain, link.RemoteAddress) return fmt.Sprintf("%s to %s (%s)", link.comm.process.String(), link.comm.Domain, link.RemoteAddress)
} }
} }

View file

@ -8,12 +8,13 @@ type Verdict uint8
// List of values a Status can have // List of values a Status can have
const ( const (
// UNDECIDED is the default status of new connections // UNDECIDED is the default status of new connections
UNDECIDED Verdict = iota VerdictUndecided Verdict = 0
ACCEPT VerdictUndeterminable Verdict = 1
BLOCK VerdictAccept Verdict = 2
DROP VerdictBlock Verdict = 3
RerouteToNameserver VerdictDrop Verdict = 4
RerouteToTunnel VerdictRerouteToNameserver Verdict = 5
VerdictRerouteToTunnel Verdict = 6
) )
// Packer Directions // Packer Directions

View file

@ -13,52 +13,52 @@ const (
ReasonUnknownProcess = "unknown connection owner: process could not be found" ReasonUnknownProcess = "unknown connection owner: process could not be found"
) )
// GetUnknownConnection returns the connection to a packet of unknown owner. // GetUnknownCommunication returns the connection to a packet of unknown owner.
func GetUnknownConnection(pkt packet.Packet) (*Connection, error) { func GetUnknownCommunication(pkt packet.Packet) (*Communication, error) {
if pkt.IsInbound() { if pkt.IsInbound() {
switch netutils.ClassifyIP(pkt.GetIPHeader().Src) { switch netutils.ClassifyIP(pkt.GetIPHeader().Src) {
case netutils.HostLocal: case netutils.HostLocal:
return getOrCreateUnknownConnection(pkt, IncomingHost) return getOrCreateUnknownCommunication(pkt, IncomingHost)
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
return getOrCreateUnknownConnection(pkt, IncomingLAN) return getOrCreateUnknownCommunication(pkt, IncomingLAN)
case netutils.Global, netutils.GlobalMulticast: case netutils.Global, netutils.GlobalMulticast:
return getOrCreateUnknownConnection(pkt, IncomingInternet) return getOrCreateUnknownCommunication(pkt, IncomingInternet)
case netutils.Invalid: case netutils.Invalid:
return getOrCreateUnknownConnection(pkt, IncomingInvalid) return getOrCreateUnknownCommunication(pkt, IncomingInvalid)
} }
} }
switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) { switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) {
case netutils.HostLocal: case netutils.HostLocal:
return getOrCreateUnknownConnection(pkt, PeerHost) return getOrCreateUnknownCommunication(pkt, PeerHost)
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast: case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
return getOrCreateUnknownConnection(pkt, PeerLAN) return getOrCreateUnknownCommunication(pkt, PeerLAN)
case netutils.Global, netutils.GlobalMulticast: case netutils.Global, netutils.GlobalMulticast:
return getOrCreateUnknownConnection(pkt, PeerInternet) return getOrCreateUnknownCommunication(pkt, PeerInternet)
case netutils.Invalid: case netutils.Invalid:
return getOrCreateUnknownConnection(pkt, PeerInvalid) return getOrCreateUnknownCommunication(pkt, PeerInvalid)
} }
// this should never happen // this should never happen
return getOrCreateUnknownConnection(pkt, PeerInvalid) return getOrCreateUnknownCommunication(pkt, PeerInvalid)
} }
func getOrCreateUnknownConnection(pkt packet.Packet, connClass string) (*Connection, error) { func getOrCreateUnknownCommunication(pkt packet.Packet, connClass string) (*Communication, error) {
connection, ok := GetConnection(process.UnknownProcess.Pid, connClass) connection, ok := GetCommunication(process.UnknownProcess.Pid, connClass)
if !ok { if !ok {
connection = &Connection{ connection = &Communication{
Domain: connClass, Domain: connClass,
Direction: pkt.IsInbound(), Direction: pkt.IsInbound(),
Verdict: DROP, Verdict: VerdictDrop,
Reason: ReasonUnknownProcess, Reason: ReasonUnknownProcess,
process: process.UnknownProcess, process: process.UnknownProcess,
Inspect: true, Inspect: true,
FirstLinkEstablished: time.Now().Unix(), FirstLinkEstablished: time.Now().Unix(),
} }
if pkt.IsOutbound() { if pkt.IsOutbound() {
connection.Verdict = BLOCK connection.Verdict = VerdictBlock
} }
} }
connection.process.AddConnection() connection.process.AddCommunication()
return connection, nil return connection, nil
} }

View file

@ -93,7 +93,7 @@ func CleanProcessStorage(thresholdDuration time.Duration) {
threshold := time.Now().Add(-thresholdDuration).Unix() threshold := time.Now().Add(-thresholdDuration).Unix()
for _, p := range processes { for _, p := range processes {
p.Lock() p.Lock()
if p.FirstConnectionEstablished < threshold && p.ConnectionCount == 0 { if p.FirstCommEstablished < threshold && p.CommCount == 0 {
go p.Delete() go p.Delete()
} }
p.Unlock() p.Unlock()

View file

@ -42,9 +42,9 @@ type Process struct {
Icon 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. // 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.
FirstConnectionEstablished int64 FirstCommEstablished int64
LastConnectionEstablished int64 LastCommEstablished int64
ConnectionCount uint CommCount uint
} }
// ProfileSet returns the assigned profile set. // ProfileSet returns the assigned profile set.
@ -66,25 +66,25 @@ func (p *Process) String() string {
return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid) return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid)
} }
// AddConnection increases the connection counter and the last connection timestamp. // AddCommunication increases the connection counter and the last connection timestamp.
func (p *Process) AddConnection() { func (p *Process) AddCommunication() {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
p.ConnectionCount++ p.CommCount++
p.LastConnectionEstablished = time.Now().Unix() p.LastCommEstablished = time.Now().Unix()
if p.FirstConnectionEstablished == 0 { if p.FirstCommEstablished == 0 {
p.FirstConnectionEstablished = p.LastConnectionEstablished p.FirstCommEstablished = p.LastCommEstablished
} }
} }
// RemoveConnection lowers the connection counter by one. // RemoveCommunication lowers the connection counter by one.
func (p *Process) RemoveConnection() { func (p *Process) RemoveCommunication() {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
if p.ConnectionCount > 0 { if p.CommCount > 0 {
p.ConnectionCount-- p.CommCount--
} }
} }

View file

@ -27,17 +27,15 @@ func makeDefaultFallbackProfile() *Profile {
Localhost: status.SecurityLevelsAll, Localhost: status.SecurityLevelsAll,
// Specials // Specials
Related: status.SecurityLevelDynamic, Related: status.SecurityLevelDynamic,
PeerToPeer: status.SecurityLevelDynamic,
}, },
ServiceEndpoints: []*EndpointPermission{ ServiceEndpoints: []*EndpointPermission{
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "", Type: EptAny,
Wildcard: true, Protocol: 0,
Protocol: 0, StartPort: 0,
StartPort: 0, EndPort: 0,
EndPort: 0, Permit: false,
Permit: false,
}, },
}, },
} }

View file

@ -2,6 +2,7 @@ package profile
import ( import (
"fmt" "fmt"
"net"
"strconv" "strconv"
"strings" "strings"
@ -13,15 +14,44 @@ type Endpoints []*EndpointPermission
// EndpointPermission holds a decision about an endpoint. // EndpointPermission holds a decision about an endpoint.
type EndpointPermission struct { type EndpointPermission struct {
DomainOrIP string Type EPType
Wildcard bool Value string
Protocol uint8
StartPort uint16 Protocol uint8
EndPort uint16 StartPort uint16
Permit bool EndPort uint16
Created int64
Permit bool
Created int64
} }
// EPType represents the type of an EndpointPermission
type EPType uint8
// EPType values
const (
EptUnknown EPType = 0
EptAny EPType = 1
EptDomain EPType = 2
EptIPv4 EPType = 3
EptIPv6 EPType = 4
EptIPv4Range EPType = 5
EptIPv6Range EPType = 6
EptASN EPType = 7
EptCountry EPType = 8
)
// EPResult represents the result of a check against an EndpointPermission
type EPResult uint8
// EndpointPermission return values
const (
NoMatch EPResult = iota
Undeterminable
Denied
Permitted
)
// IsSet returns whether the Endpoints object is "set". // IsSet returns whether the Endpoints object is "set".
func (e Endpoints) IsSet() bool { func (e Endpoints) IsSet() bool {
if len(e) > 0 { if len(e) > 0 {
@ -30,9 +60,28 @@ func (e Endpoints) IsSet() bool {
return false return false
} }
// Check checks if the given domain is governed in the list of domains and returns whether it is permitted. // CheckDomain checks the if the given endpoint matches a EndpointPermission in the list.
// If getDomainOfIP (returns reverse and forward dns matching domain name) is supplied, an IP will be resolved to a domain, if necessary. func (e Endpoints) CheckDomain(domain string) (result EPResult, reason string) {
func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (permit bool, reason string, ok bool) { if domain == "" {
return Denied, "internal error"
}
for _, entry := range e {
if entry != nil {
if result, reason = entry.MatchesDomain(domain); result != NoMatch {
return
}
}
}
return NoMatch, ""
}
// CheckIP checks the if the given endpoint matches a EndpointPermission in the list. If _checkReverseIP_ and no domain is given, the IP will be resolved to a domain, if necessary.
func (e Endpoints) CheckIP(domain string, ip net.IP, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (result EPResult, reason string) {
if ip == nil {
return Denied, "internal error"
}
// ip resolving // ip resolving
var cachedGetDomainOfIP func() string var cachedGetDomainOfIP func() string
@ -42,7 +91,7 @@ func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkRe
// setup caching wrapper // setup caching wrapper
cachedGetDomainOfIP = func() string { cachedGetDomainOfIP = func() string {
if !ipResolved { if !ipResolved {
result, err := intel.ResolveIPAndValidate(domainOrIP, securityLevel) result, err := intel.ResolveIPAndValidate(ip.String(), securityLevel)
if err != nil { if err != nil {
// log.Debug() // log.Debug()
ipName = result ipName = result
@ -53,54 +102,139 @@ func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkRe
} }
} }
isDomain := strings.HasSuffix(domainOrIP, ".")
for _, entry := range e { for _, entry := range e {
if entry != nil { if entry != nil {
if ok, reason := entry.Matches(domainOrIP, protocol, port, isDomain, cachedGetDomainOfIP); ok { if result, reason := entry.MatchesIP(domain, ip, protocol, port, cachedGetDomainOfIP); result != NoMatch {
return entry.Permit, reason, true return result, reason
} }
} }
} }
return false, "", false return NoMatch, ""
} }
func isSubdomainOf(domain, subdomain string) bool { func (ep EndpointPermission) matchesDomainOnly(domain string) (matches bool, reason string) {
dotPrefixedDomain := "." + domain wildcardInFront := strings.HasPrefix(ep.Value, "*")
return strings.HasSuffix(subdomain, dotPrefixedDomain) wildcardInBack := strings.HasSuffix(ep.Value, "*")
}
// Matches checks whether the given endpoint has a managed permission. If getDomainOfIP (returns reverse and forward dns matching domain name) is supplied, this declares an incoming connection.
func (ep EndpointPermission) Matches(domainOrIP string, protocol uint8, port uint16, isDomain bool, getDomainOfIP func() string) (match bool, reason string) {
if ep.Protocol > 0 && protocol != ep.Protocol {
return false, ""
}
if ep.StartPort > 0 && (port < ep.StartPort || port > ep.EndPort) {
return false, ""
}
switch { switch {
case ep.Wildcard && len(ep.DomainOrIP) == 0: case wildcardInFront && wildcardInBack:
// host wildcard if strings.Contains(domain, strings.Trim(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep) return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
case domainOrIP == ep.DomainOrIP: }
// host match case wildcardInFront:
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep) if strings.HasSuffix(domain, strings.TrimLeft(ep.Value, "*")) {
case isDomain && ep.Wildcard && isSubdomainOf(ep.DomainOrIP, domainOrIP): return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
// subdomain match }
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep) case wildcardInBack:
case !isDomain && getDomainOfIP != nil && getDomainOfIP() == ep.DomainOrIP: if strings.HasPrefix(domain, strings.TrimRight(ep.Value, "*")) {
// resolved IP match return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
return true, fmt.Sprintf("%s->%s matches %s", domainOrIP, getDomainOfIP(), ep) }
case !isDomain && getDomainOfIP != nil && ep.Wildcard && isSubdomainOf(ep.DomainOrIP, getDomainOfIP()):
// resolved IP subdomain match
return true, fmt.Sprintf("%s->%s matches %s", domainOrIP, getDomainOfIP(), ep)
default: default:
// no match if domain == ep.Value {
return false, "" return true, ""
}
} }
return false, ""
}
func (ep EndpointPermission) matchProtocolAndPortsAndReturn(protocol uint8, port uint16) (result EPResult) {
// only check if protocol is defined
if ep.Protocol > 0 {
// if protocol is unknown, return Undeterminable
if protocol == 0 {
return Undeterminable
}
// if protocol does not match, return NoMatch
if protocol != ep.Protocol {
return NoMatch
}
}
// only check if port is defined
if ep.StartPort > 0 {
// if port is unknown, return Undeterminable
if port == 0 {
return Undeterminable
}
// if port does not match, return NoMatch
if port < ep.StartPort || port > ep.EndPort {
return NoMatch
}
}
// protocol and port matched or were defined as any
if ep.Permit {
return Permitted
}
return Denied
}
// MatchesDomain checks if the given endpoint matches the EndpointPermission.
func (ep EndpointPermission) MatchesDomain(domain string) (result EPResult, reason string) {
switch ep.Type {
case EptAny:
// always matches
case EptDomain:
var matched bool
matched, reason = ep.matchesDomainOnly(domain)
if !matched {
return NoMatch, ""
}
case EptIPv4:
return Undeterminable, ""
case EptIPv6:
return Undeterminable, ""
case EptIPv4Range:
return Undeterminable, ""
case EptIPv6Range:
return Undeterminable, ""
case EptASN:
return Undeterminable, ""
case EptCountry:
return Undeterminable, ""
default:
return Denied, "encountered unknown enpoint permission type"
}
return ep.matchProtocolAndPortsAndReturn(0, 0), reason
}
// MatchesIP checks if the given endpoint matches the EndpointPermission. _getDomainOfIP_, if given, will be used to get the domain if not given.
func (ep EndpointPermission) MatchesIP(domain string, ip net.IP, protocol uint8, port uint16, getDomainOfIP func() string) (result EPResult, reason string) {
switch ep.Type {
case EptAny:
// always matches
case EptDomain:
if domain == "" {
if getDomainOfIP == nil {
return NoMatch, ""
}
domain = getDomainOfIP()
}
var matched bool
matched, reason = ep.matchesDomainOnly(domain)
if !matched {
return NoMatch, ""
}
case EptIPv4, EptIPv6:
if ep.Value != ip.String() {
return NoMatch, ""
}
case EptIPv4Range:
return Denied, "endpoint type IP Range not yet implemented"
case EptIPv6Range:
return Denied, "endpoint type IP Range not yet implemented"
case EptASN:
return Denied, "endpoint type ASN not yet implemented"
case EptCountry:
return Denied, "endpoint type country not yet implemented"
default:
return Denied, "encountered unknown enpoint permission type"
}
return ep.matchProtocolAndPortsAndReturn(protocol, port), reason
} }
func (e Endpoints) String() string { func (e Endpoints) String() string {
@ -111,9 +245,36 @@ func (e Endpoints) String() string {
return fmt.Sprintf("[%s]", strings.Join(s, ", ")) return fmt.Sprintf("[%s]", strings.Join(s, ", "))
} }
func (ep EndpointPermission) String() string { func (ept EPType) String() string {
s := ep.DomainOrIP switch ept {
case EptAny:
return "Any"
case EptDomain:
return "Domain"
case EptIPv4:
return "IPv4"
case EptIPv6:
return "IPv6"
case EptIPv4Range:
return "IPv4-Range"
case EptIPv6Range:
return "IPv6-Range"
case EptASN:
return "ASN"
case EptCountry:
return "Country"
default:
return "Unknown"
}
}
func (ep EndpointPermission) String() string {
s := ep.Type.String()
if ep.Type != EptAny {
s += ":"
s += ep.Value
}
s += " " s += " "
if ep.Protocol > 0 { if ep.Protocol > 0 {
@ -136,3 +297,18 @@ func (ep EndpointPermission) String() string {
return s return s
} }
func (epr EPResult) String() string {
switch epr {
case NoMatch:
return "No Match"
case Undeterminable:
return "Undeterminable"
case Denied:
return "Denied"
case Permitted:
return "Permitted"
default:
return "Unknown"
}
}

View file

@ -1,54 +1,159 @@
package profile package profile
import ( import (
"net"
"testing" "testing"
"github.com/Safing/portbase/utils/testutils"
) )
// TODO: RETIRED func testEndpointDomainMatch(t *testing.T, ep *EndpointPermission, domain string, expectedResult EPResult) {
// func testdeMatcher(t *testing.T, value string, expectedResult bool) { var result EPResult
// if domainEndingMatcher.MatchString(value) != expectedResult { result, _ = ep.MatchesDomain(domain)
// if expectedResult { if result != expectedResult {
// t.Errorf("domainEndingMatcher should match %s", value) t.Errorf(
// } else { "line %d: unexpected result for endpoint domain match %s: result=%s, expected=%s",
// t.Errorf("domainEndingMatcher should not match %s", value) testutils.GetLineNumberOfCaller(1),
// } domain,
// } result,
// } expectedResult,
// )
// func TestdomainEndingMatcher(t *testing.T) { }
// testdeMatcher(t, "example.com", true) }
// testdeMatcher(t, "com", true)
// testdeMatcher(t, "example.xn--lgbbat1ad8j", true) func testEndpointIPMatch(t *testing.T, ep *EndpointPermission, domain string, ip net.IP, protocol uint8, port uint16, expectedResult EPResult) {
// testdeMatcher(t, "xn--lgbbat1ad8j", true) var result EPResult
// testdeMatcher(t, "fe80::beef", false) result, _ = ep.MatchesIP(domain, ip, protocol, port, nil)
// testdeMatcher(t, "fe80::dead:beef", false) if result != expectedResult {
// testdeMatcher(t, "10.2.3.4", false) t.Errorf(
// testdeMatcher(t, "4", false) "line %d: unexpected result for endpoint %s/%s/%d/%d: result=%s, expected=%s",
// } testutils.GetLineNumberOfCaller(1),
domain,
ip,
protocol,
port,
result,
expectedResult,
)
}
}
func TestEndpointMatching(t *testing.T) {
ep := &EndpointPermission{
Type: EptAny,
Protocol: 0,
StartPort: 0,
EndPort: 0,
Permit: true,
}
// ANY
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
// DOMAIN
// wildcard domains
ep.Type = EptDomain
ep.Value = "*example.com."
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
ep.Type = EptDomain
ep.Value = "example.*"
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
ep.Type = EptDomain
ep.Value = "*.exampl*"
testEndpointDomainMatch(t, ep, "abc.example.com.", Permitted)
testEndpointIPMatch(t, ep, "abc.example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
ep.Value = "*.com."
testEndpointDomainMatch(t, ep, "example.com.", Permitted)
testEndpointIPMatch(t, ep, "example.com.", net.ParseIP("10.2.3.4"), 6, 443, Permitted)
// edge case
ep.Value = ""
testEndpointDomainMatch(t, ep, "example.com", NoMatch)
// edge case
ep.Value = "*"
testEndpointDomainMatch(t, ep, "example.com", Permitted)
// edge case
ep.Value = "**"
testEndpointDomainMatch(t, ep, "example.com", Permitted)
// edge case
ep.Value = "***"
testEndpointDomainMatch(t, ep, "example.com", Permitted)
// protocol
ep.Value = "example.com"
ep.Protocol = 17
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 6, 443, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
// ports
ep.StartPort = 442
ep.EndPort = 444
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
ep.StartPort = 442
ep.StartPort = 443
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
ep.StartPort = 443
ep.EndPort = 444
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
ep.StartPort = 443
ep.EndPort = 443
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
// IP
ep.Type = EptIPv4
ep.Value = "10.2.3.4"
ep.Protocol = 0
ep.StartPort = 0
ep.EndPort = 0
testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.4"), 6, 80, Permitted)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.4"), 17, 443, Permitted)
testEndpointIPMatch(t, ep, "", net.ParseIP("10.2.3.5"), 6, 80, NoMatch)
testEndpointIPMatch(t, ep, "example.com", net.ParseIP("10.2.3.5"), 17, 443, NoMatch)
testEndpointDomainMatch(t, ep, "example.com", Undeterminable)
}
func TestEPString(t *testing.T) { func TestEPString(t *testing.T) {
var endpoints Endpoints var endpoints Endpoints
endpoints = []*EndpointPermission{ endpoints = []*EndpointPermission{
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "example.com", Type: EptDomain,
Wildcard: false, Value: "example.com",
Protocol: 6, Protocol: 6,
Permit: true, Permit: true,
}, },
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "8.8.8.8", Type: EptIPv4,
Protocol: 17, // TCP Value: "1.1.1.1",
StartPort: 53, // DNS Protocol: 17, // TCP
EndPort: 53, StartPort: 53, // DNS
Permit: false, EndPort: 53,
Permit: false,
}, },
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "google.com", Type: EptDomain,
Wildcard: true, Value: "example.org",
Permit: false, Permit: false,
}, },
} }
if endpoints.String() != "[example.com 6/*, 8.8.8.8 17/53, google.com */*]" { if endpoints.String() != "[Domain:example.com 6/*, IPv4:1.1.1.1 17/53, Domain:example.org */*]" {
t.Errorf("unexpected result: %s", endpoints.String()) t.Errorf("unexpected result: %s", endpoints.String())
} }
@ -57,5 +162,4 @@ func TestEPString(t *testing.T) {
if noEndpoints.String() != "[]" { if noEndpoints.String() != "[]" {
t.Errorf("unexpected result: %s", noEndpoints.String()) t.Errorf("unexpected result: %s", noEndpoints.String())
} }
} }

View file

@ -1,13 +1,18 @@
package profile package profile
import "github.com/Safing/portbase/modules" import (
"github.com/Safing/portbase/modules"
// module dependencies
_ "github.com/Safing/portmaster/core"
)
var ( var (
shutdownSignal = make(chan struct{}) shutdownSignal = make(chan struct{})
) )
func init() { func init() {
modules.Register("profile", nil, start, stop, "global", "database") modules.Register("profile", nil, start, stop, "core")
} }
func start() error { func start() error {

View file

@ -1,6 +1,7 @@
package profile package profile
import ( import (
"net"
"sync" "sync"
"github.com/Safing/portmaster/status" "github.com/Safing/portmaster/status"
@ -119,8 +120,28 @@ func (set *Set) CheckFlag(flag uint8) (active bool) {
return false return false
} }
// CheckEndpoint checks if the given protocol and port are governed in any the lists of ports and returns whether it is permitted. // CheckEndpointDomain checks if the given endpoint matches an entry in the corresponding list. This is for outbound communication only.
func (set *Set) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, inbound bool) (permit bool, reason string, ok bool) { func (set *Set) CheckEndpointDomain(domain string) (result EPResult, reason string) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles {
if i == 2 && set.independent {
continue
}
if profile != nil {
if result, reason = profile.Endpoints.CheckDomain(domain); result != NoMatch {
return
}
}
}
return NoMatch, ""
}
// CheckEndpointIP checks if the given endpoint matches an entry in the corresponding list.
func (set *Set) CheckEndpointIP(domain string, ip net.IP, protocol uint8, port uint16, inbound bool) (result EPResult, reason string) {
set.Lock() set.Lock()
defer set.Unlock() defer set.Unlock()
@ -131,18 +152,18 @@ func (set *Set) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, in
if profile != nil { if profile != nil {
if inbound { if inbound {
if permit, reason, ok = profile.ServiceEndpoints.Check(domainOrIP, protocol, port, inbound, set.combinedSecurityLevel); ok { if result, reason = profile.ServiceEndpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch {
return return
} }
} else { } else {
if permit, reason, ok = profile.Endpoints.Check(domainOrIP, protocol, port, inbound, set.combinedSecurityLevel); ok { if result, reason = profile.Endpoints.CheckIP(domain, ip, protocol, port, inbound, set.combinedSecurityLevel); result != NoMatch {
return return
} }
} }
} }
} }
return false, "", false return NoMatch, ""
} }
// getSecurityLevel returns the highest prioritized security level. // getSecurityLevel returns the highest prioritized security level.

View file

@ -1,9 +1,11 @@
package profile package profile
import ( import (
"net"
"testing" "testing"
"time" "time"
"github.com/Safing/portbase/utils/testutils"
"github.com/Safing/portmaster/status" "github.com/Safing/portmaster/status"
) )
@ -28,31 +30,30 @@ func init() {
}, },
Endpoints: []*EndpointPermission{ Endpoints: []*EndpointPermission{
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "good.bad.example.com.", Type: EptDomain,
Wildcard: false, Value: "good.bad.example.com.",
Permit: true, Permit: true,
Created: time.Now().Unix(), Created: time.Now().Unix(),
}, },
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "bad.example.com.", Type: EptDomain,
Wildcard: true, Value: "*bad.example.com.",
Permit: false, Permit: false,
Created: time.Now().Unix(), Created: time.Now().Unix(),
}, },
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "example.com.", Type: EptDomain,
Wildcard: false, Value: "example.com.",
Permit: true, Permit: true,
Created: time.Now().Unix(), Created: time.Now().Unix(),
}, },
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "", Type: EptAny,
Wildcard: true, Permit: true,
Permit: true, Protocol: 6,
Protocol: 6, StartPort: 22000,
StartPort: 22000, EndPort: 22000,
EndPort: 22000, Created: time.Now().Unix(),
Created: time.Now().Unix(),
}, },
}, },
} }
@ -66,36 +67,33 @@ func init() {
// }, // },
Endpoints: []*EndpointPermission{ Endpoints: []*EndpointPermission{
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "bad2.example.com.", Type: EptDomain,
Wildcard: true, Value: "*bad2.example.com.",
Permit: false, Permit: false,
Created: time.Now().Unix(), Created: time.Now().Unix(),
}, },
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "", Type: EptAny,
Wildcard: true, Permit: true,
Permit: true, Protocol: 6,
Protocol: 6, StartPort: 80,
StartPort: 80, EndPort: 80,
EndPort: 80, Created: time.Now().Unix(),
Created: time.Now().Unix(),
}, },
}, },
ServiceEndpoints: []*EndpointPermission{ ServiceEndpoints: []*EndpointPermission{
&EndpointPermission{ &EndpointPermission{
DomainOrIP: "", Type: EptAny,
Wildcard: true, Permit: true,
Permit: true, Protocol: 17,
Protocol: 17, StartPort: 12345,
StartPort: 12345, EndPort: 12347,
EndPort: 12347, Created: time.Now().Unix(),
Created: time.Now().Unix(),
}, },
&EndpointPermission{ // default deny &EndpointPermission{ // default deny
DomainOrIP: "", Type: EptAny,
Wildcard: true, Permit: false,
Permit: false, Created: time.Now().Unix(),
Created: time.Now().Unix(),
}, },
}, },
} }
@ -104,25 +102,39 @@ func init() {
func testFlag(t *testing.T, set *Set, flag uint8, shouldBeActive bool) { func testFlag(t *testing.T, set *Set, flag uint8, shouldBeActive bool) {
active := set.CheckFlag(flag) active := set.CheckFlag(flag)
if active != shouldBeActive { if active != shouldBeActive {
t.Errorf("unexpected result: flag %s: permitted=%v, expected=%v", flagNames[flag], active, shouldBeActive) t.Errorf("unexpected result: flag %s: active=%v, expected=%v", flagNames[flag], active, shouldBeActive)
} }
} }
func testEndpoint(t *testing.T, set *Set, domainOrIP string, protocol uint8, port uint16, inbound bool, shouldBePermitted bool) { func testEndpointDomain(t *testing.T, set *Set, domain string, expectedResult EPResult) {
var permitted, ok bool var result EPResult
permitted, _, ok = set.CheckEndpoint(domainOrIP, protocol, port, inbound) result, _ = set.CheckEndpointDomain(domain)
if !ok { if result != expectedResult {
t.Errorf("endpoint %s/%d/%d/%v should be in test profile set", domainOrIP, protocol, port, inbound) t.Errorf(
} "line %d: unexpected result for endpoint domain %s: result=%s, expected=%s",
if permitted != shouldBePermitted { testutils.GetLineNumberOfCaller(1),
t.Errorf("unexpected result for endpoint %s/%d/%d/%v: permitted=%v, expected=%v", domainOrIP, protocol, port, inbound, permitted, shouldBePermitted) domain,
result,
expectedResult,
)
} }
} }
func testUnregulatedEndpoint(t *testing.T, set *Set, domainOrIP string, protocol uint8, port uint16, inbound bool) { func testEndpointIP(t *testing.T, set *Set, domain string, ip net.IP, protocol uint8, port uint16, inbound bool, expectedResult EPResult) {
_, _, ok := set.CheckEndpoint(domainOrIP, protocol, port, inbound) var result EPResult
if ok { result, _ = set.CheckEndpointIP(domain, ip, protocol, port, inbound)
t.Errorf("endpoint %s/%d/%d/%v should not be in test profile set", domainOrIP, protocol, port, inbound) if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint %s/%s/%d/%d/%v: result=%s, expected=%s",
testutils.GetLineNumberOfCaller(1),
domain,
ip,
protocol,
port,
inbound,
result,
expectedResult,
)
} }
} }
@ -133,28 +145,28 @@ func TestProfileSet(t *testing.T) {
set.Update(status.SecurityLevelDynamic) set.Update(status.SecurityLevelDynamic)
testFlag(t, set, Whitelist, false) testFlag(t, set, Whitelist, false)
// testFlag(t, set, Internet, true) // testFlag(t, set, Internet, true)
testEndpoint(t, set, "example.com.", 0, 0, false, true) testEndpointDomain(t, set, "example.com.", Permitted)
testEndpoint(t, set, "bad.example.com.", 0, 0, false, false) testEndpointDomain(t, set, "bad.example.com.", Denied)
testEndpoint(t, set, "other.bad.example.com.", 0, 0, false, false) testEndpointDomain(t, set, "other.bad.example.com.", Denied)
testEndpoint(t, set, "good.bad.example.com.", 0, 0, false, true) testEndpointDomain(t, set, "good.bad.example.com.", Permitted)
testEndpoint(t, set, "bad2.example.com.", 0, 0, false, false) testEndpointDomain(t, set, "bad2.example.com.", Undeterminable)
testEndpoint(t, set, "10.2.3.4", 6, 22000, false, true) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 22000, false, Permitted)
testEndpoint(t, set, "fd00::1", 6, 22000, false, true) testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 6, 22000, false, Permitted)
testEndpoint(t, set, "test.local.", 6, 22000, false, true) testEndpointDomain(t, set, "test.local.", Undeterminable)
testUnregulatedEndpoint(t, set, "other.example.com.", 0, 0, false) testEndpointDomain(t, set, "other.example.com.", Undeterminable)
testUnregulatedEndpoint(t, set, "10.2.3.4", 17, 53, false) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 53, false, NoMatch)
testUnregulatedEndpoint(t, set, "10.2.3.4", 17, 443, false) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 443, false, NoMatch)
testUnregulatedEndpoint(t, set, "10.2.3.4", 6, 12346, false) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 12346, false, NoMatch)
testEndpoint(t, set, "10.2.3.4", 17, 12345, true, true) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Permitted)
testEndpoint(t, set, "fd00::1", 17, 12347, true, true) testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Permitted)
set.Update(status.SecurityLevelSecure) set.Update(status.SecurityLevelSecure)
// testFlag(t, set, Internet, true) // testFlag(t, set, Internet, true)
set.Update(status.SecurityLevelFortress) // Independent! set.Update(status.SecurityLevelFortress) // Independent!
testFlag(t, set, Whitelist, true) testFlag(t, set, Whitelist, true)
testEndpoint(t, set, "10.2.3.4", 17, 12345, true, false) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 17, 12345, true, Denied)
testEndpoint(t, set, "fd00::1", 17, 12347, true, false) testEndpointIP(t, set, "", net.ParseIP("fd00::1"), 17, 12347, true, Denied)
testUnregulatedEndpoint(t, set, "10.2.3.4", 6, 80, false) testEndpointIP(t, set, "", net.ParseIP("10.2.3.4"), 6, 80, false, NoMatch)
testUnregulatedEndpoint(t, set, "bad2.example.com.", 0, 0, false) testEndpointDomain(t, set, "bad2.example.com.", Undeterminable)
} }

View file

@ -48,8 +48,7 @@ func getSpecialProfile(ID string) (*Profile, error) {
func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) { func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
for _, ep := range p.ServiceEndpoints { for _, ep := range p.ServiceEndpoints {
if ep != nil { if ep != nil {
if ep.DomainOrIP == "" && if ep.Type == EptAny &&
ep.Wildcard == true &&
ep.Protocol == 0 && ep.Protocol == 0 &&
ep.StartPort == 0 && ep.StartPort == 0 &&
ep.EndPort == 0 && ep.EndPort == 0 &&
@ -60,12 +59,11 @@ func ensureServiceEndpointsDenyAll(p *Profile) (changed bool) {
} }
p.ServiceEndpoints = append(p.ServiceEndpoints, &EndpointPermission{ p.ServiceEndpoints = append(p.ServiceEndpoints, &EndpointPermission{
DomainOrIP: "", Type: EptAny,
Wildcard: true, Protocol: 0,
Protocol: 0, StartPort: 0,
StartPort: 0, EndPort: 0,
EndPort: 0, Permit: false,
Permit: false,
}) })
return true return true
} }

View file

@ -2,6 +2,7 @@ package profile
import ( import (
"strings" "strings"
"sync/atomic"
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
"github.com/Safing/portbase/database/query" "github.com/Safing/portbase/database/query"
@ -56,11 +57,27 @@ func updateListener(sub *database.Subscription) {
switch { switch {
case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")): case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")):
updateActiveUserProfile(profile) updateActiveUserProfile(profile)
increaseUpdateVersion()
case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")): case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")):
updateActiveStampProfile(profile) updateActiveStampProfile(profile)
increaseUpdateVersion()
} }
} }
} }
} }
} }
var (
updateVersion uint32
)
// GetUpdateVersion returns the current profiles internal update version
func GetUpdateVersion() uint32 {
return atomic.LoadUint32(&updateVersion)
}
func increaseUpdateVersion() {
// we intentially want to wrap
atomic.AddUint32(&updateVersion, 1)
}

View file

@ -18,16 +18,16 @@ func max(a, b uint8) uint8 {
// ConfigIsActive returns whether the given security level dependent config option is on or off. // ConfigIsActive returns whether the given security level dependent config option is on or off.
func ConfigIsActive(name string) SecurityLevelOption { func ConfigIsActive(name string) SecurityLevelOption {
activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic)) activeAtLevel := config.GetAsInt(name, int64(SecurityLevelsAll))
return func(minSecurityLevel uint8) bool { return func(minSecurityLevel uint8) bool {
return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel) return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0
} }
} }
// ConfigIsActiveConcurrent returns whether the given security level dependent config option is on or off and is concurrency safe. // ConfigIsActiveConcurrent returns whether the given security level dependent config option is on or off and is concurrency safe.
func ConfigIsActiveConcurrent(name string) SecurityLevelOption { func ConfigIsActiveConcurrent(name string) SecurityLevelOption {
activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelDynamic)) activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll))
return func(minSecurityLevel uint8) bool { return func(minSecurityLevel uint8) bool {
return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel) return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0
} }
} }

View file

@ -4,6 +4,9 @@ import (
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/Safing/portbase/modules" "github.com/Safing/portbase/modules"
// module dependencies
_ "github.com/Safing/portmaster/core"
) )
var ( var (
@ -11,7 +14,7 @@ var (
) )
func init() { func init() {
modules.Register("status", nil, start, stop, "database") modules.Register("status", nil, start, stop, "core")
} }
func start() error { func start() error {

6
threats/all.go Normal file
View file

@ -0,0 +1,6 @@
package threats
import (
_ "github.com/Safing/portmaster/threats/arp"
_ "github.com/Safing/portmaster/threats/portscan"
)

7
threats/arp/arpentry.go Normal file
View file

@ -0,0 +1,7 @@
package arp
type arpEntry struct {
IP string
MAC string
Interface string
}

48
threats/arp/os_linux.go Normal file
View file

@ -0,0 +1,48 @@
package arp
import (
"bufio"
"os"
"strings"
"github.com/Safing/portbase/log"
)
const (
arpTableProcFile = "/proc/net/arp"
)
func getArpTable() (table []*arpEntry, err error) {
// open file
arpData, err := os.Open(arpTableProcFile)
if err != nil {
log.Warningf("threats/arp: could not read %s: %s", arpTableProcFile, err)
return nil, err
}
defer arpData.Close()
// file scanner
scanner := bufio.NewScanner(arpData)
scanner.Split(bufio.ScanLines)
// parse
scanner.Scan() // skip first line
for scanner.Scan() {
line := strings.Fields(scanner.Text())
if len(line) < 6 {
continue
}
table = append(table, &arpEntry{
IP: line[0],
MAC: line[3],
Interface: line[5],
})
}
return table, nil
}
func clearArpTable() error {
return nil
}

View file

@ -0,0 +1 @@
package portscan

View file

@ -0,0 +1 @@
package portscan

View file

@ -103,6 +103,7 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri
return return
} }
// set content type
_, ok := w.Header()["Content-Type"] _, ok := w.Header()["Content-Type"]
if !ok { if !ok {
contentType := mime.TypeByExtension(filepath.Ext(path)) contentType := mime.TypeByExtension(filepath.Ext(path))
@ -111,6 +112,9 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri
} }
} }
// set content security policy
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if r.Method != "HEAD" { if r.Method != "HEAD" {
_, err = io.Copy(w, readCloser) _, err = io.Copy(w, readCloser)

View file

@ -7,7 +7,11 @@ import (
"runtime" "runtime"
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
"github.com/Safing/portbase/info"
"github.com/Safing/portbase/modules" "github.com/Safing/portbase/modules"
// module dependencies
_ "github.com/Safing/portmaster/core"
) )
var ( var (
@ -15,10 +19,11 @@ var (
) )
func init() { func init() {
modules.Register("updates", prep, start, nil, "global", "database") modules.Register("updates", prep, start, nil, "core")
} }
func prep() error { func prep() error {
status.Core = info.GetInfo()
updateStoragePath = filepath.Join(database.GetDatabaseRoot(), "updates") updateStoragePath = filepath.Join(database.GetDatabaseRoot(), "updates")
err := checkUpdateDirs() err := checkUpdateDirs()

View file

@ -7,6 +7,7 @@ import (
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
"github.com/Safing/portbase/database/query" "github.com/Safing/portbase/database/query"
"github.com/Safing/portbase/database/record" "github.com/Safing/portbase/database/record"
"github.com/Safing/portbase/info"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/tevino/abool" "github.com/tevino/abool"
) )
@ -36,7 +37,7 @@ var (
func init() { func init() {
status = &versionStatus{ status = &versionStatus{
Versions: make(map[string]*versionStatusEntry), Modules: make(map[string]*versionStatusEntry),
} }
status.SetKey(statusDBKey) status.SetKey(statusDBKey)
} }
@ -45,7 +46,9 @@ func init() {
type versionStatus struct { type versionStatus struct {
record.Base record.Base
sync.Mutex sync.Mutex
Versions map[string]*versionStatusEntry
Core *info.Info
Modules map[string]*versionStatusEntry
} }
func (vs *versionStatus) save() { func (vs *versionStatus) save() {
@ -68,10 +71,10 @@ func updateUsedStatus(identifier string, version string) {
status.Lock() status.Lock()
defer status.Unlock() defer status.Unlock()
entry, ok := status.Versions[identifier] entry, ok := status.Modules[identifier]
if !ok { if !ok {
entry = &versionStatusEntry{} entry = &versionStatusEntry{}
status.Versions[identifier] = entry status.Modules[identifier] = entry
} }
entry.LastVersionUsed = version entry.LastVersionUsed = version
@ -87,10 +90,10 @@ func updateStatus(vClass versionClass, state map[string]string) {
for identifier, version := range state { for identifier, version := range state {
entry, ok := status.Versions[identifier] entry, ok := status.Modules[identifier]
if !ok { if !ok {
entry = &versionStatusEntry{} entry = &versionStatusEntry{}
status.Versions[identifier] = entry status.Modules[identifier] = entry
} }
switch vClass { switch vClass {