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

View file

@ -2,10 +2,13 @@ package firewall
import (
"github.com/Safing/portbase/config"
"github.com/Safing/portmaster/status"
)
var (
permanentVerdicts config.BoolOption
permanentVerdicts config.BoolOption
filterDNSByScope status.SecurityLevelOption
filterDNSByProfile status.SecurityLevelOption
)
func registerConfig() error {
@ -22,5 +25,35 @@ func registerConfig() error {
}
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
}

View file

@ -13,6 +13,10 @@ import (
"github.com/Safing/portmaster/firewall/interception"
"github.com/Safing/portmaster/network"
"github.com/Safing/portmaster/network/packet"
// module dependencies
_ "github.com/Safing/portmaster/core"
_ "github.com/Safing/portmaster/profile"
)
var (
@ -37,7 +41,7 @@ var (
)
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) {
@ -111,7 +115,7 @@ func handlePacket(pkt packet.Packet) {
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
// timed := time.Now()
@ -146,43 +150,51 @@ func handlePacket(pkt packet.Packet) {
func initialHandler(pkt packet.Packet, link *network.Link) {
// get Connection
connection, err := network.GetConnectionByFirstPacket(pkt)
// get Communication
comm, err := network.GetCommunicationByFirstPacket(pkt)
if err != nil {
// get "unknown" connection
// get "unknown" comm
link.Deny(fmt.Sprintf("could not get process: %s", err))
connection, err = network.GetUnknownConnection(pkt)
comm, err = network.GetUnknownCommunication(pkt)
if err != nil {
// all failed
log.Errorf("firewall: could not get unknown connection (dropping %s): %s", pkt.String(), err)
link.UpdateVerdict(network.DROP)
verdict(pkt, network.DROP)
log.Errorf("firewall: could not get unknown comm (dropping %s): %s", pkt.String(), err)
link.UpdateVerdict(network.VerdictDrop)
verdict(pkt, network.VerdictDrop)
link.StopFirewallHandler()
return
}
}
// add new Link to Connection (and save both)
connection.AddLink(link)
// add new Link to Communication (and save both)
comm.AddLink(link)
// 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()
verdict(pkt, link.GetVerdict())
link.StopFirewallHandler()
return
}
// make a decision if not made already
if connection.GetVerdict() == network.UNDECIDED {
DecideOnConnection(connection, pkt)
// check if communication needs reevaluation
if comm.NeedsReevaluation() {
comm.ResetVerdict()
}
if connection.GetVerdict() == network.ACCEPT {
DecideOnLink(connection, link, pkt)
} else {
link.UpdateVerdict(connection.GetVerdict())
// make a decision if not made already
switch comm.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
@ -201,7 +213,7 @@ func initialHandler(pkt packet.Packet, link *network.Link) {
// // tunnel link, don't inspect
// link.Tunneled = true
// link.StopFirewallHandler()
// permanentVerdict(pkt, network.ACCEPT)
// permanentVerdict(pkt, network.VerdictAccept)
case link.Inspect:
link.SetFirewallHandler(inspectThenVerdict)
inspectThenVerdict(pkt, link)
@ -241,22 +253,22 @@ func inspectThenVerdict(pkt packet.Packet, link *network.Link) {
func permanentVerdict(pkt packet.Packet, action network.Verdict) {
switch action {
case network.ACCEPT:
case network.VerdictAccept:
atomic.AddUint64(packetsAccepted, 1)
pkt.PermanentAccept()
return
case network.BLOCK:
case network.VerdictBlock:
atomic.AddUint64(packetsBlocked, 1)
pkt.PermanentBlock()
return
case network.DROP:
case network.VerdictDrop:
atomic.AddUint64(packetsDropped, 1)
pkt.PermanentDrop()
return
case network.RerouteToNameserver:
case network.VerdictRerouteToNameserver:
pkt.RerouteToNameserver()
return
case network.RerouteToTunnel:
case network.VerdictRerouteToTunnel:
pkt.RerouteToTunnel()
return
}
@ -265,22 +277,22 @@ func permanentVerdict(pkt packet.Packet, action network.Verdict) {
func verdict(pkt packet.Packet, action network.Verdict) {
switch action {
case network.ACCEPT:
case network.VerdictAccept:
atomic.AddUint64(packetsAccepted, 1)
pkt.Accept()
return
case network.BLOCK:
case network.VerdictBlock:
atomic.AddUint64(packetsBlocked, 1)
pkt.Block()
return
case network.DROP:
case network.VerdictDrop:
atomic.AddUint64(packetsDropped, 1)
pkt.Drop()
return
case network.RerouteToNameserver:
case network.VerdictRerouteToNameserver:
pkt.RerouteToNameserver()
return
case network.RerouteToTunnel:
case network.VerdictRerouteToTunnel:
pkt.RerouteToTunnel()
return
}
@ -302,26 +314,26 @@ func verdict(pkt packet.Packet, action network.Verdict) {
func logInitialVerdict(link *network.Link) {
// switch link.GetVerdict() {
// case network.ACCEPT:
// case network.VerdictAccept:
// log.Infof("firewall: accepting new link: %s", link.String())
// case network.BLOCK:
// case network.VerdictBlock:
// log.Infof("firewall: blocking new link: %s", link.String())
// case network.DROP:
// case network.VerdictDrop:
// 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())
// case network.RerouteToTunnel:
// case network.VerdictRerouteToTunnel:
// log.Infof("firewall: rerouting new link to tunnel: %s", link.String())
// }
}
func logChangedVerdict(link *network.Link) {
// switch link.GetVerdict() {
// case network.ACCEPT:
// case network.VerdictAccept:
// 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())
// case network.DROP:
// case network.VerdictDrop:
// 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
verdict := network.UNDECIDED
verdict := network.VerdictUndecided
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)
switch action {
case DO_NOTHING:
if verdict < network.ACCEPT {
verdict = network.ACCEPT
if verdict < network.VerdictAccept {
verdict = network.VerdictAccept
}
continueInspection = true
case BLOCK_PACKET:
if verdict < network.BLOCK {
verdict = network.BLOCK
if verdict < network.VerdictBlock {
verdict = network.VerdictBlock
}
continueInspection = true
case DROP_PACKET:
verdict = network.DROP
verdict = network.VerdictDrop
continueInspection = true
case BLOCK_LINK:
link.UpdateVerdict(network.BLOCK)
link.UpdateVerdict(network.VerdictBlock)
activeInspectors[key] = true
if verdict < network.BLOCK {
verdict = network.BLOCK
if verdict < network.VerdictBlock {
verdict = network.VerdictBlock
}
case DROP_LINK:
link.UpdateVerdict(network.DROP)
link.UpdateVerdict(network.VerdictDrop)
activeInspectors[key] = true
verdict = network.DROP
verdict = network.VerdictDrop
case STOP_INSPECTING:
activeInspectors[key] = true
}

View file

@ -2,85 +2,89 @@ package firewall
import (
"fmt"
"net"
"os"
"strings"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/intel"
"github.com/Safing/portmaster/network"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/network/packet"
"github.com/Safing/portmaster/profile"
"github.com/Safing/portmaster/status"
"github.com/miekg/dns"
"github.com/agext/levenshtein"
)
// 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
// 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
// 3. DecideOnConnection
// is called when the first packet of the first link of the connection arrives
// 3. DecideOnCommunication
// is called when the first packet of the first link of the communication arrives
// 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.
func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) {
// check:
// Profile.DomainWhitelist
// Profile.Flags
// - process specific: System, Admin, User
// - network specific: Internet, LocalNet
// DecideOnCommunicationBeforeIntel makes a decision about a communication before the dns query is resolved and intel is gathered.
func DecideOnCommunicationBeforeIntel(comm *network.Communication, fqdn string) {
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection)
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 := connection.Process().ProfileSet()
// get and check profile set
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying connection %s, no Profile Set", connection)
connection.Deny("no Profile Set")
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// check for any network access
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, accessing Internet or LAN not allowed", connection)
connection.Deny("accessing Internet or LAN not allowed")
log.Infof("firewall: denying communication %s, accessing Internet or LAN not permitted", comm)
comm.Deny("accessing Internet or LAN not permitted")
return
}
// check domain list
permitted, reason, ok := profileSet.CheckEndpoint(fqdn, 0, 0, false)
if ok {
if permitted {
log.Infof("firewall: accepting connection %s, endpoint is whitelisted: %s", connection, reason)
connection.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
} else {
log.Infof("firewall: denying connection %s, endpoint is blacklisted: %s", connection, reason)
connection.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
}
// check endpoint list
result, reason := profileSet.CheckEndpointDomain(fqdn)
switch result {
case profile.NoMatch:
comm.UpdateVerdict(network.VerdictUndecided)
case profile.Undeterminable:
comm.UpdateVerdict(network.VerdictUndeterminable)
return
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
}
switch profileSet.GetProfileMode() {
case profile.Whitelist:
log.Infof("firewall: denying connection %s, domain is not whitelisted", connection)
connection.Deny("domain is not whitelisted")
log.Infof("firewall: denying communication %s, domain is not whitelisted", comm)
comm.Deny("domain is not whitelisted")
return
case profile.Prompt:
// check Related flag
// TODO: improve this!
if profileSet.CheckFlag(profile.Related) {
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
if len(pathElements) > 2 {
pathElements = pathElements[len(pathElements)-2:]
@ -104,167 +108,285 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string)
processElement = profileSet.UserProfile().Name
break matchLoop
}
if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 {
if levenshtein.Match(domainElement, comm.Process().Name, nil) > 0.5 {
matched = true
processElement = connection.Process().Name
processElement = comm.Process().Name
break matchLoop
}
if levenshtein.Match(domainElement, connection.Process().ExecName, nil) > 0.5 {
if levenshtein.Match(domainElement, comm.Process().ExecName, nil) > 0.5 {
matched = true
processElement = connection.Process().ExecName
processElement = comm.Process().ExecName
break matchLoop
}
}
if matched {
log.Infof("firewall: accepting connection %s, match to domain was found: %s ~== %s", connection, domainElement, processElement)
connection.Accept("domain is related to process")
log.Infof("firewall: permitting communication %s, match to domain was found: %s ~== %s", comm, domainElement, processElement)
comm.Accept("domain is related to process")
}
}
if connection.GetVerdict() != network.ACCEPT {
if comm.GetVerdict() != network.VerdictAccept {
// TODO
log.Infof("firewall: accepting connection %s, domain permitted (prompting is not yet implemented)", connection)
connection.Accept("domain permitted (prompting is not yet implemented)")
log.Infof("firewall: permitting communication %s, domain permitted (prompting is not yet implemented)", comm)
comm.Accept("domain permitted (prompting is not yet implemented)")
}
return
case profile.Blacklist:
log.Infof("firewall: accepting connection %s, domain is not blacklisted", connection)
connection.Accept("domain is not blacklisted")
log.Infof("firewall: permitting communication %s, domain is not blacklisted", comm)
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.
func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache {
// DecideOnCommunicationAfterIntel makes a decision about a communication after the dns query is resolved and intel is gathered.
func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, rrCache *intel.RRCache) {
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection)
connection.Accept("")
// SUSPENDED until Stamp integration is finished
// grant self - should not get here
// 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
}
// check if there is a profile
profileSet := connection.Process().ProfileSet()
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying connection %s, no Profile Set", connection)
connection.Deny("no Profile Set")
return rrCache
log.Infof("firewall: blocking dns query of communication %s, no Profile Set", comm)
return nil
}
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
// 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
}
// DeciceOnConnection makes a decision about a connection with its first packet.
func DecideOnConnection(connection *network.Connection, pkt packet.Packet) {
// DecideOnCommunication makes a decision about a communication with its first packet.
func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) {
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection)
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 := connection.Process().ProfileSet()
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying connection %s, no Profile Set", connection)
connection.Deny("no Profile Set")
log.Errorf("firewall: denying communication %s, no Profile Set", comm)
comm.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// check connection type
switch connection.Domain {
// check comm type
switch comm.Domain {
case network.IncomingHost, network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
if !profileSet.CheckFlag(profile.Service) {
log.Infof("firewall: denying connection %s, not a service", connection)
if connection.Domain == network.IncomingHost {
connection.Block("not a service")
log.Infof("firewall: denying communication %s, not a service", comm)
if comm.Domain == network.IncomingHost {
comm.Block("not a service")
} else {
connection.Drop("not a service")
comm.Drop("not a service")
}
return
}
case network.PeerLAN, network.PeerInternet, network.PeerInvalid: // Important: PeerHost is and should be missing!
if !profileSet.CheckFlag(profile.PeerToPeer) {
log.Infof("firewall: denying connection %s, peer to peer connections (to an IP) not allowed", connection)
connection.Deny("peer to peer connections (to an IP) not allowed")
log.Infof("firewall: denying communication %s, peer to peer comms (to an IP) not allowed", comm)
comm.Deny("peer to peer comms (to an IP) not allowed")
return
}
default:
}
// check network scope
switch connection.Domain {
switch comm.Domain {
case network.IncomingHost:
if !profileSet.CheckFlag(profile.Localhost) {
log.Infof("firewall: denying connection %s, serving localhost not allowed", connection)
connection.Block("serving localhost not allowed")
log.Infof("firewall: denying communication %s, serving localhost not allowed", comm)
comm.Block("serving localhost not allowed")
return
}
case network.IncomingLAN:
if !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, serving LAN not allowed", connection)
connection.Deny("serving LAN not allowed")
log.Infof("firewall: denying communication %s, serving LAN not allowed", comm)
comm.Deny("serving LAN not allowed")
return
}
case network.IncomingInternet:
if !profileSet.CheckFlag(profile.Internet) {
log.Infof("firewall: denying connection %s, serving Internet not allowed", connection)
connection.Deny("serving Internet not allowed")
log.Infof("firewall: denying communication %s, serving Internet not allowed", comm)
comm.Deny("serving Internet not allowed")
return
}
case network.IncomingInvalid:
log.Infof("firewall: denying connection %s, invalid IP address", connection)
connection.Drop("invalid IP address")
log.Infof("firewall: denying communication %s, invalid IP address", comm)
comm.Drop("invalid IP address")
return
case network.PeerHost:
if !profileSet.CheckFlag(profile.Localhost) {
log.Infof("firewall: denying connection %s, accessing localhost not allowed", connection)
connection.Block("accessing localhost not allowed")
log.Infof("firewall: denying communication %s, accessing localhost not allowed", comm)
comm.Block("accessing localhost not allowed")
return
}
case network.PeerLAN:
if !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, accessing the LAN not allowed", connection)
connection.Deny("accessing the LAN not allowed")
log.Infof("firewall: denying communication %s, accessing the LAN not allowed", comm)
comm.Deny("accessing the LAN not allowed")
return
}
case network.PeerInternet:
if !profileSet.CheckFlag(profile.Internet) {
log.Infof("firewall: denying connection %s, accessing the Internet not allowed", connection)
connection.Deny("accessing the Internet not allowed")
log.Infof("firewall: denying communication %s, accessing the Internet not allowed", comm)
comm.Deny("accessing the Internet not allowed")
return
}
case network.PeerInvalid:
log.Infof("firewall: denying connection %s, invalid IP address", connection)
connection.Deny("invalid IP address")
log.Infof("firewall: denying communication %s, invalid IP address", comm)
comm.Deny("invalid IP address")
return
}
log.Infof("firewall: accepting connection %s", connection)
connection.Accept("")
log.Infof("firewall: undeterminable verdict for communication %s", comm)
}
// 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:
// Profile.Flags
// - network specific: Internet, LocalNet
@ -272,34 +394,37 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
// Profile.ListenPorts
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own link %s", connection)
connection.Accept("")
if comm.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own link %s", comm)
link.Accept("")
return
}
// check if there is a profile
profileSet := connection.Process().ProfileSet()
profileSet := comm.Process().ProfileSet()
if profileSet == nil {
log.Infof("firewall: no Profile Set, denying %s", link)
link.Block("no Profile Set")
link.Deny("no Profile Set")
return
}
profileSet.Update(status.ActiveSecurityLevel())
// get host
var domainOrIP string
switch {
case strings.HasSuffix(connection.Domain, "."):
domainOrIP = connection.Domain
case connection.Direction:
domainOrIP = pkt.GetIPHeader().Src.String()
default:
domainOrIP = pkt.GetIPHeader().Dst.String()
// get domain
var domain string
if strings.HasSuffix(comm.Domain, ".") {
domain = comm.Domain
}
// get protocol / destination port
protocol := pkt.GetIPHeader().Protocol
// remoteIP
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
tcpUDPHeader := pkt.GetTCPUDPHeader()
if tcpUDPHeader != nil {
@ -307,15 +432,17 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
}
// check endpoints list
permitted, reason, ok := profileSet.CheckEndpoint(domainOrIP, uint8(protocol), dstPort, connection.Direction)
if ok {
if permitted {
log.Infof("firewall: accepting link %s, endpoint is whitelisted: %s", link, reason)
link.Accept(fmt.Sprintf("port whitelisted: %s", reason))
} else {
log.Infof("firewall: denying link %s: endpoint is blacklisted: %s", link, reason)
link.Deny("port blacklisted")
}
result, reason := profileSet.CheckEndpointIP(domain, remoteIP, protocol, dstPort, comm.Direction)
switch result {
// case profile.NoMatch, profile.Undeterminable:
// continue
case profile.Denied:
log.Infof("firewall: denying link %s, endpoint is blacklisted: %s", link, reason)
link.Deny(fmt.Sprintf("endpoint is blacklisted: %s", reason))
return
case profile.Permitted:
log.Infof("firewall: permitting link %s, endpoint is whitelisted: %s", link, reason)
link.Accept(fmt.Sprintf("endpoint is whitelisted: %s", reason))
return
}
@ -325,15 +452,15 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
link.Deny("endpoint is not whitelisted")
return
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)")
return
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")
return
}
log.Infof("firewall: accepting link %s", link)
link.Accept("")
log.Infof("firewall: denying link %s, no profile mode set", link)
link.Deny("no profile mode set")
}

View file

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

View file

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

View file

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

View file

@ -15,7 +15,6 @@ import (
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/status"
)
@ -304,13 +303,6 @@ func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype
}
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
}
@ -357,11 +349,13 @@ func query(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
}
new := &RRCache{
Domain: fqdn,
Question: qtype,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Domain: fqdn,
Question: qtype,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Server: resolver.Server,
ServerScope: resolver.ServerIPScope,
}
// TODO: check if reply.Answer is valid

View file

@ -5,11 +5,8 @@ package intel
import (
"fmt"
"net"
"strings"
"time"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/miekg/dns"
)
@ -23,10 +20,14 @@ type RRCache struct {
Extra []dns.RR
TTL int64
Server string
ServerScope int8
updated int64
servedFromCache bool
requestingNew bool
Filtered bool
FilteredEntries []string
}
// 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.
func (m *RRCache) ToNameRecord() *NameRecord {
new := &NameRecord{
Domain: m.Domain,
Question: m.Question.String(),
TTL: m.TTL,
Filtered: m.Filtered,
Domain: m.Domain,
Question: m.Question.String(),
TTL: m.TTL,
Server: m.Server,
ServerScope: m.ServerScope,
}
// 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
return rrCache, nil
}
@ -175,82 +178,23 @@ func (m *RRCache) IsNXDomain() bool {
return len(m.Answer) == 0
}
// Duplicate returns a duplicate of the cache. slices are not copied, but referenced.
func (m *RRCache) Duplicate() *RRCache {
// ShallowCopy returns a shallow copy of the cache. slices are not copied, but referenced.
func (m *RRCache) ShallowCopy() *RRCache {
return &RRCache{
Domain: m.Domain,
Question: m.Question,
Answer: m.Answer,
Ns: m.Ns,
Extra: m.Extra,
TTL: m.TTL,
Domain: m.Domain,
Question: m.Question,
Answer: m.Answer,
Ns: m.Ns,
Extra: m.Extra,
TTL: m.TTL,
Server: m.Server,
ServerScope: m.ServerScope,
updated: m.updated,
servedFromCache: m.servedFromCache,
requestingNew: m.requestingNew,
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"
// include packages here
_ "github.com/Safing/portbase/api"
_ "github.com/Safing/portbase/database/dbmodule"
_ "github.com/Safing/portbase/database/storage/badger"
_ "github.com/Safing/portmaster/core"
_ "github.com/Safing/portmaster/firewall"
_ "github.com/Safing/portmaster/nameserver"
_ "github.com/Safing/portmaster/ui"

View file

@ -126,8 +126,8 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
// get connection
// start = time.Now()
connection, err := network.GetConnectionByDNSRequest(rAddr.IP, uint16(rAddr.Port), fqdn)
// log.Tracef("nameserver: took %s to get connection (and maybe process)", time.Since(start))
comm, err := network.GetCommunicationByDNSRequest(rAddr.IP, uint16(rAddr.Port), fqdn)
// log.Tracef("nameserver: took %s to get comms (and maybe process)", time.Since(start))
if err != nil {
log.Warningf("nameserver: someone is requesting %s, but could not identify process: %s, returning nxdomain", fqdn, err)
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
// 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
if connection.GetVerdict() == network.UNDECIDED {
if comm.GetVerdict() == network.VerdictUndecided || comm.GetVerdict() == network.VerdictUndeterminable {
// start = time.Now()
firewall.DecideOnConnectionBeforeIntel(connection, fqdn)
firewall.DecideOnCommunicationBeforeIntel(comm, fqdn)
// 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)
return
}
// get intel and RRs
// 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))
if rrCache == nil {
// 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)
return
}
// set intel
connection.Lock()
connection.Intel = domainIntel
connection.Unlock()
connection.Save()
comm.Lock()
comm.Intel = domainIntel
comm.Unlock()
comm.Save()
// do a full check with intel
if connection.GetVerdict() == network.UNDECIDED {
rrCache = firewall.DecideOnConnectionAfterIntel(connection, fqdn, rrCache)
if comm.GetVerdict() == network.VerdictUndecided || comm.GetVerdict() == network.VerdictUndeterminable {
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)
return
}

View file

@ -20,7 +20,7 @@ func cleaner() {
cleanLinks()
time.Sleep(2 * time.Second)
cleanConnections()
cleanComms()
time.Sleep(2 * time.Second)
cleanProcesses()
}
@ -73,18 +73,18 @@ func cleanLinks() {
}
}
func cleanConnections() {
connectionsLock.RLock()
defer connectionsLock.RUnlock()
func cleanComms() {
commsLock.RLock()
defer commsLock.RUnlock()
threshold := time.Now().Add(-thresholdDuration).Unix()
for _, conn := range connections {
conn.Lock()
if conn.FirstLinkEstablished < threshold && conn.LinkCount == 0 {
// log.Tracef("network.clean: deleted %s", conn.DatabaseKey())
go conn.Delete()
for _, comm := range comms {
comm.Lock()
if comm.FirstLinkEstablished < threshold && comm.LinkCount == 0 {
// log.Tracef("network.clean: deleted %s", comm.DatabaseKey())
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 (
links = make(map[string]*Link)
linksLock sync.RWMutex
connections = make(map[string]*Connection)
connectionsLock sync.RWMutex
links = make(map[string]*Link)
linksLock sync.RWMutex
comms = make(map[string]*Communication)
commsLock sync.RWMutex
dbController *database.Controller
)
@ -43,9 +43,9 @@ func (s *StorageInterface) Get(key string) (record.Record, error) {
}
}
case 3:
connectionsLock.RLock()
defer connectionsLock.RUnlock()
conn, ok := connections[splitted[2]]
commsLock.RLock()
defer commsLock.RUnlock()
conn, ok := comms[splitted[2]]
if ok {
return conn, nil
}
@ -79,14 +79,14 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
}
}
// connections
connectionsLock.RLock()
for _, conn := range connections {
// comms
commsLock.RLock()
for _, conn := range comms {
if strings.HasPrefix(conn.DatabaseKey(), q.DatabaseKeyPrefix()) {
it.Next <- conn
}
}
connectionsLock.RUnlock()
commsLock.RUnlock()
// links
linksLock.RLock()

View file

@ -38,18 +38,18 @@ type Link struct {
pktQueue chan packet.Packet
firewallHandler FirewallHandler
connection *Connection
comm *Communication
activeInspectors []bool
inspectorData map[uint8]interface{}
}
// Connection returns the Connection the Link is part of
func (link *Link) Connection() *Connection {
// Communication returns the Communication the Link is part of
func (link *Link) Communication() *Communication {
link.Lock()
defer link.Unlock()
return link.connection
return link.comm
}
// 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.
func (link *Link) Accept(reason string) {
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.
func (link *Link) Deny(reason string) {
if link.connection != nil && link.connection.Direction {
if link.comm != nil && link.comm.Direction {
link.Drop(reason)
} else {
link.Block(reason)
@ -121,24 +121,24 @@ func (link *Link) Deny(reason string) {
// Block blocks the link and adds the given reason.
func (link *Link) Block(reason string) {
link.AddReason(reason)
link.UpdateVerdict(BLOCK)
link.UpdateVerdict(VerdictBlock)
}
// Drop drops the link and adds the given reason.
func (link *Link) Drop(reason string) {
link.AddReason(reason)
link.UpdateVerdict(DROP)
link.UpdateVerdict(VerdictDrop)
}
// RerouteToNameserver reroutes the link to the portmaster nameserver.
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.
func (link *Link) RerouteToTunnel(reason string) {
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
@ -192,30 +192,30 @@ func (link *Link) ApplyVerdict(pkt packet.Packet) {
if link.VerdictPermanent {
switch link.Verdict {
case ACCEPT:
case VerdictAccept:
pkt.PermanentAccept()
case BLOCK:
case VerdictBlock:
pkt.PermanentBlock()
case DROP:
case VerdictDrop:
pkt.PermanentDrop()
case RerouteToNameserver:
case VerdictRerouteToNameserver:
pkt.RerouteToNameserver()
case RerouteToTunnel:
case VerdictRerouteToTunnel:
pkt.RerouteToTunnel()
default:
pkt.Drop()
}
} else {
switch link.Verdict {
case ACCEPT:
case VerdictAccept:
pkt.Accept()
case BLOCK:
case VerdictBlock:
pkt.Block()
case DROP:
case VerdictDrop:
pkt.Drop()
case RerouteToNameserver:
case VerdictRerouteToNameserver:
pkt.RerouteToNameserver()
case RerouteToTunnel:
case VerdictRerouteToTunnel:
pkt.RerouteToTunnel()
default:
pkt.Drop()
@ -228,12 +228,12 @@ func (link *Link) Save() error {
link.Lock()
defer link.Unlock()
if link.connection == nil {
return errors.New("cannot save link without connection")
if link.comm == nil {
return errors.New("cannot save link without comms")
}
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()
}
@ -262,8 +262,8 @@ func (link *Link) Delete() {
link.Meta().Delete()
go dbController.PushUpdate(link)
link.connection.RemoveLink()
go link.connection.Save()
link.comm.RemoveLink()
go link.comm.Save()
}
// 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 {
link := &Link{
ID: pkt.GetLinkID(),
Verdict: UNDECIDED,
Verdict: VerdictUndecided,
Started: time.Now().Unix(),
RemoteAddress: pkt.FmtRemoteAddress(),
}
@ -332,24 +332,24 @@ func (link *Link) String() string {
link.Lock()
defer link.Unlock()
if link.connection == nil {
if link.comm == nil {
return fmt.Sprintf("? <-> %s", link.RemoteAddress)
}
switch link.connection.Domain {
switch link.comm.Domain {
case "I":
if link.connection.process == nil {
if link.comm.process == nil {
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":
if link.connection.process == nil {
if link.comm.process == nil {
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:
if link.connection.process == nil {
return fmt.Sprintf("? -> %s (%s)", link.connection.Domain, link.RemoteAddress)
if link.comm.process == nil {
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
const (
// UNDECIDED is the default status of new connections
UNDECIDED Verdict = iota
ACCEPT
BLOCK
DROP
RerouteToNameserver
RerouteToTunnel
VerdictUndecided Verdict = 0
VerdictUndeterminable Verdict = 1
VerdictAccept Verdict = 2
VerdictBlock Verdict = 3
VerdictDrop Verdict = 4
VerdictRerouteToNameserver Verdict = 5
VerdictRerouteToTunnel Verdict = 6
)
// Packer Directions

View file

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

View file

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

View file

@ -42,9 +42,9 @@ type Process struct {
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.
FirstConnectionEstablished int64
LastConnectionEstablished int64
ConnectionCount uint
FirstCommEstablished int64
LastCommEstablished int64
CommCount uint
}
// 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)
}
// AddConnection increases the connection counter and the last connection timestamp.
func (p *Process) AddConnection() {
// AddCommunication increases the connection counter and the last connection timestamp.
func (p *Process) AddCommunication() {
p.Lock()
defer p.Unlock()
p.ConnectionCount++
p.LastConnectionEstablished = time.Now().Unix()
if p.FirstConnectionEstablished == 0 {
p.FirstConnectionEstablished = p.LastConnectionEstablished
p.CommCount++
p.LastCommEstablished = time.Now().Unix()
if p.FirstCommEstablished == 0 {
p.FirstCommEstablished = p.LastCommEstablished
}
}
// RemoveConnection lowers the connection counter by one.
func (p *Process) RemoveConnection() {
// RemoveCommunication lowers the connection counter by one.
func (p *Process) RemoveCommunication() {
p.Lock()
defer p.Unlock()
if p.ConnectionCount > 0 {
p.ConnectionCount--
if p.CommCount > 0 {
p.CommCount--
}
}

View file

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

View file

@ -2,6 +2,7 @@ package profile
import (
"fmt"
"net"
"strconv"
"strings"
@ -13,15 +14,44 @@ type Endpoints []*EndpointPermission
// EndpointPermission holds a decision about an endpoint.
type EndpointPermission struct {
DomainOrIP string
Wildcard bool
Protocol uint8
StartPort uint16
EndPort uint16
Permit bool
Created int64
Type EPType
Value string
Protocol uint8
StartPort uint16
EndPort uint16
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".
func (e Endpoints) IsSet() bool {
if len(e) > 0 {
@ -30,9 +60,28 @@ func (e Endpoints) IsSet() bool {
return false
}
// Check checks if the given domain is governed in the list of domains and returns whether it is permitted.
// 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) Check(domainOrIP string, protocol uint8, port uint16, checkReverseIP bool, securityLevel uint8) (permit bool, reason string, ok bool) {
// CheckDomain checks the if the given endpoint matches a EndpointPermission in the list.
func (e Endpoints) CheckDomain(domain string) (result EPResult, reason string) {
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
var cachedGetDomainOfIP func() string
@ -42,7 +91,7 @@ func (e Endpoints) Check(domainOrIP string, protocol uint8, port uint16, checkRe
// setup caching wrapper
cachedGetDomainOfIP = func() string {
if !ipResolved {
result, err := intel.ResolveIPAndValidate(domainOrIP, securityLevel)
result, err := intel.ResolveIPAndValidate(ip.String(), securityLevel)
if err != nil {
// log.Debug()
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 {
if entry != nil {
if ok, reason := entry.Matches(domainOrIP, protocol, port, isDomain, cachedGetDomainOfIP); ok {
return entry.Permit, reason, true
if result, reason := entry.MatchesIP(domain, ip, protocol, port, cachedGetDomainOfIP); result != NoMatch {
return result, reason
}
}
}
return false, "", false
return NoMatch, ""
}
func isSubdomainOf(domain, subdomain string) bool {
dotPrefixedDomain := "." + domain
return strings.HasSuffix(subdomain, dotPrefixedDomain)
}
// 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, ""
}
func (ep EndpointPermission) matchesDomainOnly(domain string) (matches bool, reason string) {
wildcardInFront := strings.HasPrefix(ep.Value, "*")
wildcardInBack := strings.HasSuffix(ep.Value, "*")
switch {
case ep.Wildcard && len(ep.DomainOrIP) == 0:
// host wildcard
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep)
case domainOrIP == ep.DomainOrIP:
// host match
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep)
case isDomain && ep.Wildcard && isSubdomainOf(ep.DomainOrIP, domainOrIP):
// subdomain match
return true, fmt.Sprintf("%s matches %s", domainOrIP, ep)
case !isDomain && getDomainOfIP != nil && getDomainOfIP() == ep.DomainOrIP:
// resolved IP match
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)
case wildcardInFront && wildcardInBack:
if strings.Contains(domain, strings.Trim(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
case wildcardInFront:
if strings.HasSuffix(domain, strings.TrimLeft(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
case wildcardInBack:
if strings.HasPrefix(domain, strings.TrimRight(ep.Value, "*")) {
return true, fmt.Sprintf("%s matches %s", domain, ep.Value)
}
default:
// no match
return false, ""
if domain == ep.Value {
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 {
@ -111,9 +245,36 @@ func (e Endpoints) String() string {
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
}
func (ep EndpointPermission) String() string {
s := ep.DomainOrIP
func (ept EPType) String() string {
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 += " "
if ep.Protocol > 0 {
@ -136,3 +297,18 @@ func (ep EndpointPermission) String() string {
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
import (
"net"
"testing"
"github.com/Safing/portbase/utils/testutils"
)
// TODO: RETIRED
// func testdeMatcher(t *testing.T, value string, expectedResult bool) {
// if domainEndingMatcher.MatchString(value) != expectedResult {
// if expectedResult {
// t.Errorf("domainEndingMatcher should match %s", value)
// } else {
// t.Errorf("domainEndingMatcher should not match %s", value)
// }
// }
// }
//
// func TestdomainEndingMatcher(t *testing.T) {
// testdeMatcher(t, "example.com", true)
// testdeMatcher(t, "com", true)
// testdeMatcher(t, "example.xn--lgbbat1ad8j", true)
// testdeMatcher(t, "xn--lgbbat1ad8j", true)
// testdeMatcher(t, "fe80::beef", false)
// testdeMatcher(t, "fe80::dead:beef", false)
// testdeMatcher(t, "10.2.3.4", false)
// testdeMatcher(t, "4", false)
// }
func testEndpointDomainMatch(t *testing.T, ep *EndpointPermission, domain string, expectedResult EPResult) {
var result EPResult
result, _ = ep.MatchesDomain(domain)
if result != expectedResult {
t.Errorf(
"line %d: unexpected result for endpoint domain match %s: result=%s, expected=%s",
testutils.GetLineNumberOfCaller(1),
domain,
result,
expectedResult,
)
}
}
func testEndpointIPMatch(t *testing.T, ep *EndpointPermission, domain string, ip net.IP, protocol uint8, port uint16, expectedResult EPResult) {
var result EPResult
result, _ = ep.MatchesIP(domain, ip, protocol, port, nil)
if result != expectedResult {
t.Errorf(
"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) {
var endpoints Endpoints
endpoints = []*EndpointPermission{
&EndpointPermission{
DomainOrIP: "example.com",
Wildcard: false,
Protocol: 6,
Permit: true,
Type: EptDomain,
Value: "example.com",
Protocol: 6,
Permit: true,
},
&EndpointPermission{
DomainOrIP: "8.8.8.8",
Protocol: 17, // TCP
StartPort: 53, // DNS
EndPort: 53,
Permit: false,
Type: EptIPv4,
Value: "1.1.1.1",
Protocol: 17, // TCP
StartPort: 53, // DNS
EndPort: 53,
Permit: false,
},
&EndpointPermission{
DomainOrIP: "google.com",
Wildcard: true,
Permit: false,
Type: EptDomain,
Value: "example.org",
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())
}
@ -57,5 +162,4 @@ func TestEPString(t *testing.T) {
if noEndpoints.String() != "[]" {
t.Errorf("unexpected result: %s", noEndpoints.String())
}
}

View file

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

View file

@ -1,6 +1,7 @@
package profile
import (
"net"
"sync"
"github.com/Safing/portmaster/status"
@ -119,8 +120,28 @@ func (set *Set) CheckFlag(flag uint8) (active bool) {
return false
}
// CheckEndpoint checks if the given protocol and port are governed in any the lists of ports and returns whether it is permitted.
func (set *Set) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, inbound bool) (permit bool, reason string, ok bool) {
// CheckEndpointDomain checks if the given endpoint matches an entry in the corresponding list. This is for outbound communication only.
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()
defer set.Unlock()
@ -131,18 +152,18 @@ func (set *Set) CheckEndpoint(domainOrIP string, protocol uint8, port uint16, in
if profile != nil {
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
}
} 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 false, "", false
return NoMatch, ""
}
// getSecurityLevel returns the highest prioritized security level.

View file

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

View file

@ -2,6 +2,7 @@ package profile
import (
"strings"
"sync/atomic"
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/database/query"
@ -56,11 +57,27 @@ func updateListener(sub *database.Subscription) {
switch {
case strings.HasPrefix(profile.Key(), MakeProfileKey(UserNamespace, "")):
updateActiveUserProfile(profile)
increaseUpdateVersion()
case strings.HasPrefix(profile.Key(), MakeProfileKey(StampNamespace, "")):
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.
func ConfigIsActive(name string) SecurityLevelOption {
activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic))
activeAtLevel := config.GetAsInt(name, int64(SecurityLevelsAll))
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.
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 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/log"
"github.com/Safing/portbase/modules"
// module dependencies
_ "github.com/Safing/portmaster/core"
)
var (
@ -11,7 +14,7 @@ var (
)
func init() {
modules.Register("status", nil, start, stop, "database")
modules.Register("status", nil, start, stop, "core")
}
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
}
// set content type
_, ok := w.Header()["Content-Type"]
if !ok {
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)
if r.Method != "HEAD" {
_, err = io.Copy(w, readCloser)

View file

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

View file

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