Working on portmaster restructure

This commit is contained in:
Daniel 2018-12-07 21:28:45 +01:00
parent 8fb21fd900
commit 8c11a35590
24 changed files with 850 additions and 554 deletions

View file

@ -1,15 +1,14 @@
package firewall
import (
"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/status"
"github.com/agext/levenshtein"
)
@ -25,6 +24,7 @@ import (
// 4. DecideOnLink
// is called when when the first packet of a link arrives only if connection 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
@ -35,244 +35,227 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string)
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection)
connection.Accept()
connection.Accept("")
return
}
// check if there is a profile
profileSet := connection.Process().ProfileSetSet
if profile == nil {
log.Infof("firewall: no profile, denying connection %s", connection)
connection.AddReason("no profile")
connection.Block()
profileSet := connection.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying connection %s, no profile set", connection)
connection.Deny("no profile set")
return
}
// check user class
if profileSet.CheckFlag(profile.System) {
if !connection.Process().IsSystem() {
log.Infof("firewall: denying connection %s, profile has System flag set, but process is not executed by System", connection)
connection.AddReason("must be executed by system")
connection.Block()
return
}
}
if profileSet.CheckFlag(profile.Admin) {
if !connection.Process().IsAdmin() {
log.Infof("firewall: denying connection %s, profile has Admin flag set, but process is not executed by Admin", connection)
connection.AddReason("must be executed by admin")
connection.Block()
return
}
}
if profileSet.CheckFlag(profile.User) {
if !connection.Process().IsUser() {
log.Infof("firewall: denying connection %s, profile has User flag set, but process is not executed by a User", connection)
connection.AddReason("must be executed by user")
connection.Block()
return
}
}
profileSet.Update(status.CurrentSecurityLevel())
// check for any network access
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LocalNet) {
log.Infof("firewall: denying connection %s, profile denies Internet and local network access", connection)
connection.Block()
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")
return
}
// check domain whitelist/blacklist
if len(profile.DomainWhitelist) > 0 {
matched := false
for _, entry := range profile.DomainWhitelist {
if !strings.HasSuffix(entry, ".") {
entry += "."
}
if strings.HasPrefix(entry, "*") {
if strings.HasSuffix(fqdn, strings.Trim(entry, "*")) {
matched = true
break
}
} else {
if entry == fqdn {
matched = true
break
}
}
}
if matched {
if profile.DomainWhitelistIsBlacklist {
log.Infof("firewall: denying connection %s, profile has %s in domain blacklist", connection, fqdn)
connection.AddReason("domain blacklisted")
connection.Block()
return
}
// check domain list
permitted, ok := profileSet.CheckDomain(fqdn)
if ok {
if permitted {
log.Infof("firewall: accepting connection %s, domain is whitelisted", connection, domainElement, processElement)
connection.Accept("domain is whitelisted")
} else {
if !profile.DomainWhitelistIsBlacklist {
log.Infof("firewall: denying connection %s, profile does not have %s in domain whitelist", connection, fqdn)
connection.AddReason("domain not in whitelist")
connection.Block()
return
log.Infof("firewall: denying connection %s, domain is blacklisted", connection, domainElement, processElement)
connection.Deny("domain is blacklisted")
}
return
}
switch profileSet.GetProfileMode() {
case profile.Whitelist:
log.Infof("firewall: denying connection %s, domain is not whitelisted", connection, domainElement, processElement)
connection.Deny("domain is not whitelisted")
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
// only look at the last two path segments
if len(pathElements) > 2 {
pathElements = pathElements[len(pathElements)-2:]
}
}
}
domainElements := strings.Split(fqdn, ".")
}
var domainElement string
var processElement string
func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache {
// check:
// TODO: Profile.ClassificationBlacklist
// TODO: Profile.ClassificationWhitelist
// Profile.Flags
// - network specific: Strict
// check if there is a profile
profileSet := connection.Process().ProfileSet
// FIXME: there should always be a profile
if profileSet == nil {
log.Infof("firewall: no profile, denying connection %s", connection)
connection.AddReason("no profile")
connection.Block()
return rrCache
}
// check Strict flag
// TODO: drastically improve this!
if profileSet.CheckFlag(profile.Related) {
matched := false
pathElements := strings.Split(connection.Process().Path, "/")
if len(pathElements) > 2 {
pathElements = pathElements[len(pathElements)-2:]
}
domainElements := strings.Split(fqdn, ".")
matchLoop:
for _, domainElement := range domainElements {
for _, pathElement := range pathElements {
if levenshtein.Match(domainElement, pathElement, nil) > 0.5 {
matchLoop:
for _, domainElement = range domainElements {
for _, pathElement := range pathElements {
if levenshtein.Match(domainElement, pathElement, nil) > 0.5 {
matched = true
processElement = pathElement
break matchLoop
}
}
if levenshtein.Match(domainElement, profile.Name, nil) > 0.5 {
matched = true
processElement = profile.Name
break matchLoop
}
if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 {
matched = true
processElement = connection.Process().Name
break matchLoop
}
}
if levenshtein.Match(domainElement, profile.Name, nil) > 0.5 {
matched = true
break matchLoop
}
if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 {
matched = true
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")
}
}
if !matched {
log.Infof("firewall: denying connection %s, profile has declared Strict flag and no match to domain was found", connection)
connection.AddReason("domain does not relate to process")
connection.Block()
return rrCache
if connection.Verdict != network.ACCEPT {
// TODO
log.Infof("firewall: accepting connection %s, domain permitted (prompting is not yet implemented)", connection, domainElement, processElement)
connection.Accept("domain permitted (prompting is not yet implemented)")
}
case profile.Blacklist:
log.Infof("firewall: denying connection %s, domain is not blacklisted", connection, domainElement, processElement)
connection.Deny("domain is not blacklisted")
}
// tunneling
// TODO: link this to real status
// gate17Active := mode.Client()
// if gate17Active {
// tunnelInfo, err := AssignTunnelIP(fqdn)
// if err != nil {
// log.Errorf("portmaster: could not get tunnel IP for routing %s: %s", connection, err)
// return nil // return nxDomain
// }
// // save original reply
// tunnelInfo.RRCache = rrCache
// // return tunnel IP
// return tunnelInfo.ExportTunnelIP()
// }
return rrCache
}
func DecideOnConnection(connection *network.Connection, pkt packet.Packet) {
// check:
// Profile.Flags
// - process specific: System, Admin, User
// - network specific: Internet, LocalNet, Service, Directconnect
// 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 {
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection)
connection.Accept()
connection.Accept("")
return rrCache
}
// check if there is a profile
profileSet := connection.Process().ProfileSet()
if profileSet == nil {
log.Errorf("firewall: denying connection %s, no profile set", connection)
connection.Deny("no profile")
return rrCache
}
profileSet.Update(status.CurrentSecurityLevel())
// TODO: Stamp integration
// 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) {
// grant self
if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection)
connection.Accept("")
return
}
// check if there is a profile
profileSet := connection.Process().ProfileSet
if profile == nil {
log.Infof("firewall: no profile, denying connection %s", connection)
connection.AddReason("no profile")
connection.Block()
return
}
// check user class
if profileSet.CheckFlag(profile.System) {
if !connection.Process().IsSystem() {
log.Infof("firewall: denying connection %s, profile has System flag set, but process is not executed by System", connection)
connection.AddReason("must be executed by system")
connection.Block()
return
}
}
if profileSet.CheckFlag(profile.Admin) {
if !connection.Process().IsAdmin() {
log.Infof("firewall: denying connection %s, profile has Admin flag set, but process is not executed by Admin", connection)
connection.AddReason("must be executed by admin")
connection.Block()
return
}
}
if profileSet.CheckFlag(profile.User) {
if !connection.Process().IsUser() {
log.Infof("firewall: denying connection %s, profile has User flag set, but process is not executed by a User", connection)
connection.AddReason("must be executed by user")
connection.Block()
return
}
}
// check for any network access
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LocalNet) {
log.Infof("firewall: denying connection %s, profile denies Internet and local network access", connection)
connection.AddReason("no network access allowed")
connection.Block()
log.Errorf("firewall: denying connection %s, no profile set", connection)
connection.Deny("no profile")
return
}
profileSet.Update(status.CurrentSecurityLevel())
// check connection type
switch connection.Domain {
case "I":
// check Service flag
case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
if !profileSet.CheckFlag(profile.Service) {
log.Infof("firewall: denying connection %s, profile does not declare service", connection)
connection.AddReason("not a service")
connection.Drop()
log.Infof("firewall: denying connection %s, not a service", connection)
if connection.Domain == IncomingHost {
connection.Block("not a service")
} else {
connection.Drop("not a service")
}
return
}
// check if incoming connections are allowed on any port, but only if there no other restrictions
if !!profileSet.CheckFlag(profile.Internet) && !!profileSet.CheckFlag(profile.LocalNet) && len(profile.ListenPorts) == 0 {
log.Infof("firewall: granting connection %s, profile allows incoming connections from anywhere and on any port", connection)
connection.Accept()
return
}
case "D":
// check PeerToPeer flag
case PeerLAN, PeerInternet, PeerInvalid: // Important: PeerHost is and should be missing!
if !profileSet.CheckFlag(profile.PeerToPeer) {
log.Infof("firewall: denying connection %s, profile does not declare direct connections", connection)
connection.AddReason("direct connections (without DNS) not allowed")
connection.Drop()
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")
return
}
}
log.Infof("firewall: could not decide on connection %s, deciding on per-link basis", connection)
connection.CantSay()
// check network scope
switch connection.Domain {
case IncomingHost:
if !profileSet.CheckFlag(profile.Localhost) {
log.Infof("firewall: denying connection %s, serving localhost not allowed", connection)
connection.Block("serving localhost not allowed")
return
}
case IncomingLAN:
if !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, serving LAN not allowed", connection)
connection.Deny("serving LAN not allowed")
return
}
case IncomingInternet:
if !profileSet.CheckFlag(profile.Internet) {
log.Infof("firewall: denying connection %s, serving Internet not allowed", connection)
connection.Deny("serving Internet not allowed")
return
}
case IncomingInvalid:
log.Infof("firewall: denying connection %s, invalid IP address", connection)
connection.Drop("invalid IP address")
return
case PeerHost:
if !profileSet.CheckFlag(profile.Localhost) {
log.Infof("firewall: denying connection %s, accessing localhost not allowed", connection)
connection.Block("accessing localhost not allowed")
return
}
case 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")
return
}
case 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")
return
}
case PeerInvalid:
log.Infof("firewall: denying connection %s, invalid IP address", connection)
connection.Deny("invalid IP address")
return
}
log.Infof("firewall: accepting connection %s", connection)
connection.Accept()
}
// DecideOnLink makes a decision about a link with the first packet.
func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet.Packet) {
// check:
// Profile.Flags
@ -284,107 +267,44 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
profileSet := connection.Process().ProfileSet
if profile == nil {
log.Infof("firewall: no profile, denying %s", link)
link.AddReason("no profile")
link.UpdateVerdict(network.BLOCK)
link.Block("no profile")
return
}
profileSet.Update(status.CurrentSecurityLevel())
// get remote Port
protocol := pkt.GetIPHeader().Protocol
var remotePort uint16
tcpUdpHeader := pkt.GetTCPUDPHeader()
if tcpUdpHeader != nil {
remotePort = tcpUdpHeader.DstPort
}
// check port list
permitted, ok := profileSet.CheckPort(connection.Direction, protocol, remotePort)
if ok {
if permitted {
log.Infof("firewall: accepting link %s", link)
link.Accept("port whitelisted")
} else {
log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort)
link.Deny("port blacklisted")
}
return
}
// check LocalNet and Internet flags
var remoteIP net.IP
if connection.Direction {
remoteIP = pkt.GetIPHeader().Src
} else {
remoteIP = pkt.GetIPHeader().Dst
}
if netutils.IPIsLocal(remoteIP) {
if !profileSet.CheckFlag(profile.LocalNet) {
log.Infof("firewall: dropping link %s, profile does not allow communication in the local network", link)
link.AddReason("profile does not allow access to local network")
link.UpdateVerdict(network.BLOCK)
return
}
} else {
if !profileSet.CheckFlag(profile.Internet) {
log.Infof("firewall: dropping link %s, profile does not allow communication with the Internet", link)
link.AddReason("profile does not allow access to the Internet")
link.UpdateVerdict(network.BLOCK)
return
}
}
// check connect ports
if connection.Domain != "I" && len(profile.ConnectPorts) > 0 {
tcpUdpHeader := pkt.GetTCPUDPHeader()
if tcpUdpHeader == nil {
log.Infof("firewall: blocking link %s, profile has declared connect port whitelist, but link is not TCP/UDP", link)
link.AddReason("profile has declared connect port whitelist, but link is not TCP/UDP")
link.UpdateVerdict(network.BLOCK)
return
}
// packet *should* be outbound, but we could be deciding on an already active connection.
var remotePort uint16
if connection.Direction {
remotePort = tcpUdpHeader.SrcPort
} else {
remotePort = tcpUdpHeader.DstPort
}
matched := false
for _, port := range profile.ConnectPorts {
if remotePort == port {
matched = true
break
}
}
if !matched {
log.Infof("firewall: blocking link %s, remote port %d not in profile connect port whitelist", link, remotePort)
link.AddReason("destination port not in whitelist")
link.UpdateVerdict(network.BLOCK)
return
}
}
// check listen ports
if connection.Domain == "I" && len(profile.ListenPorts) > 0 {
tcpUdpHeader := pkt.GetTCPUDPHeader()
if tcpUdpHeader == nil {
log.Infof("firewall: dropping link %s, profile has declared listen port whitelist, but link is not TCP/UDP", link)
link.AddReason("profile has declared listen port whitelist, but link is not TCP/UDP")
link.UpdateVerdict(network.DROP)
return
}
// packet *should* be inbound, but we could be deciding on an already active connection.
var localPort uint16
if connection.Direction {
localPort = tcpUdpHeader.DstPort
} else {
localPort = tcpUdpHeader.SrcPort
}
matched := false
for _, port := range profile.ListenPorts {
if localPort == port {
matched = true
break
}
}
if !matched {
log.Infof("firewall: blocking link %s, local port %d not in profile listen port whitelist", link, localPort)
link.AddReason("listen port not in whitelist")
link.UpdateVerdict(network.BLOCK)
return
}
switch profileSet.GetProfileMode() {
case profile.Whitelist:
log.Infof("firewall: denying link %s: port %d is not whitelisted", link, remotePort)
link.Deny("port is not whitelisted")
case profile.Prompt:
log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort)
link.Accept("port permitted (prompting is not yet implemented)")
case profile.Blacklist:
log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort)
link.Deny("port is not blacklisted")
}
log.Infof("firewall: accepting link %s", link)
link.UpdateVerdict(network.ACCEPT)
link.Accept("")
}

View file

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

View file

@ -15,6 +15,7 @@ import (
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/status"
)
@ -76,26 +77,6 @@ func Resolve(fqdn string, qtype dns.Type, securityLevel uint8) *RRCache {
// timed := time.Now()
// defer log.Tracef("intel: took %s to get resolve %s%s", time.Now().Sub(timed).String(), fqdn, qtype.String())
// handle request for localhost
if fqdn == "localhost." {
var rr dns.RR
var err error
switch uint16(qtype) {
case dns.TypeA:
rr, err = dns.NewRR("localhost. 17 IN A 127.0.0.1")
case dns.TypeAAAA:
rr, err = dns.NewRR("localhost. 17 IN AAAA ::1")
default:
return nil
}
if err != nil {
return nil
}
return &RRCache{
Answer: []dns.RR{rr},
}
}
// check cache
rrCache, err := GetRRCache(fqdn, qtype)
if err != nil {
@ -322,6 +303,14 @@ func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype
return nil, false
}
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
}
@ -368,11 +357,11 @@ func query(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
}
new := &RRCache{
Domain: fqdn,
Domain: fqdn,
Question: qtype,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
Answer: reply.Answer,
Ns: reply.Ns,
Extra: reply.Extra,
}
// TODO: check if reply.Answer is valid

View file

@ -26,6 +26,7 @@ type Resolver struct {
ServerType string
ServerAddress string
ServerIP net.IP
ServerIPScope int8
ServerPort uint16
VerifyDomain string
Source string
@ -151,6 +152,7 @@ configuredServersLoop:
ServerType: parts[0],
ServerAddress: parts[1],
ServerIP: ip,
ServerIPScope: netutils.ClassifyAddress(ip),
ServerPort: port,
LastFail: &lastFail,
Source: "config",
@ -205,6 +207,7 @@ assignedServersLoop:
ServerType: "dns",
ServerAddress: urlFormatAddress(nameserver.IP, 53),
ServerIP: nameserver.IP,
ServerIPScope: netutils.ClassifyAddress(nameserver.IP),
ServerPort: 53,
LastFail: &lastFail,
Source: "dhcp",
@ -213,7 +216,7 @@ assignedServersLoop:
}
new.clientManager = newDNSClientManager(new)
if netutils.IPIsLocal(nameserver.IP) && len(nameserver.Search) > 0 {
if netutils.IPIsLAN(nameserver.IP) && len(nameserver.Search) > 0 {
// only allow searches for local resolvers
var newSearch []string
for _, value := range nameserver.Search {
@ -236,7 +239,7 @@ assignedServersLoop:
// make list with local resolvers
localResolvers = make([]*Resolver, 0)
for _, resolver := range globalResolvers {
if resolver.ServerIP != nil && netutils.IPIsLocal(resolver.ServerIP) {
if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) {
localResolvers = append(localResolvers, resolver)
}
}

View file

@ -3,9 +3,13 @@
package intel
import (
"fmt"
"net"
"strings"
"time"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/miekg/dns"
)
@ -22,6 +26,7 @@ type RRCache struct {
updated int64
servedFromCache bool
requestingNew bool
Filtered bool
}
// Clean sets all TTLs to 17 and sets cache expiry with specified minimum.
@ -77,6 +82,7 @@ func (m *RRCache) ToNameRecord() *NameRecord {
Domain: m.Domain,
Question: m.Question.String(),
TTL: m.TTL,
Filtered: m.Filtered,
}
// stringify RR entries
@ -130,6 +136,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
}
}
rrCache.Filtered = nameRecord.Filtered
rrCache.servedFromCache = true
return rrCache, nil
}
@ -146,19 +153,104 @@ func (m *RRCache) RequestingNew() bool {
// Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format.
func (m *RRCache) Flags() string {
switch {
case m.servedFromCache && m.requestingNew:
return " [CR]"
case m.servedFromCache:
return " [C]"
case m.requestingNew:
return " [R]" // should never enter this state, but let's leave it here, just in case
default:
return ""
var s string
if m.servedFromCache {
s += "C"
}
if m.requestingNew {
s += "R"
}
if m.Filtered {
s += "F"
}
if s != "" {
return fmt.Sprintf(" [%s]", s)
}
return ""
}
// IsNXDomain returnes whether the result is nxdomain.
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 {
return &RRCache{
Domain: m.Domain,
Question: m.Question,
Answer: m.Answer,
Ns: m.Ns,
Extra: m.Extra,
TTL: m.TTL,
updated: m.updated,
servedFromCache: m.servedFromCache,
requestingNew: m.requestingNew,
Filtered: m.Filtered,
}
}
// 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.ClassifyAddress(v.A)
case *dns.AAAA:
classification = netutils.ClassifyAddress(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

@ -18,8 +18,28 @@ import (
"github.com/Safing/portmaster/firewall"
)
var (
localhostIPs []dns.RR
)
func init() {
modules.Register("nameserver", nil, start, nil, "intel")
modules.Register("nameserver", prep, start, nil, "intel")
}
func prep() error {
localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1")
if err != nil {
return err
}
localhostIPv6, err := dns.NewRR("localhost. 17 IN AAAA ::1")
if err != nil {
return err
}
localhostIPs = []dns.RR{localhostIPv4, localhostIPv6}
return nil
}
func start() error {
@ -49,7 +69,6 @@ func nxDomain(w dns.ResponseWriter, query *dns.Msg) {
func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
// TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain
// TODO: handle securityLevelOff
// only process first question, that's how everyone does it.
question := query.Question[0]
@ -84,6 +103,14 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
return
}
// handle request for localhost
if fqdn == "localhost." {
m := new(dns.Msg)
m.SetReply(query)
m.Answer = localhostIPs
w.WriteMsg(m)
}
// get remote address
// start := time.Now()
rAddr, ok := w.RemoteAddr().(*net.UDPAddr)

View file

@ -38,51 +38,57 @@ func (conn *Connection) Process() *process.Process {
return conn.process
}
// CantSay sets the connection verdict to "can't say", the connection will be further analysed.
func (conn *Connection) CantSay() {
if conn.Verdict != CANTSAY {
conn.Verdict = CANTSAY
conn.Save()
}
return
// Accept accepts the connection and adds the given reason.
func (conn *Link) Accept(reason string) {
conn.AddReason(reason)
conn.UpdateVerdict(ACCEPT)
}
// Drop sets the connection verdict to drop.
func (conn *Connection) Drop() {
if conn.Verdict != DROP {
conn.Verdict = DROP
conn.Save()
// Deny blocks or drops the connection depending on the connection direction and adds the given reason.
func (conn *Link) Deny(reason string) {
if conn.Direction {
conn.Drop(reason)
} else {
conn.Block(reason)
}
return
}
// Block sets the connection verdict to block.
func (conn *Connection) Block() {
if conn.Verdict != BLOCK {
conn.Verdict = BLOCK
conn.Save()
}
return
// Block blocks the connection and adds the given reason.
func (conn *Link) Block(reason string) {
conn.AddReason(reason)
conn.UpdateVerdict(BLOCK)
}
// Accept sets the connection verdict to accept.
func (conn *Connection) Accept() {
if conn.Verdict != ACCEPT {
conn.Verdict = ACCEPT
// Drop drops the connection and adds the given reason.
func (conn *Link) 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
conn.Save()
}
return
}
// AddReason adds a human readable string as to why a certain verdict was set in regard to this connection
func (conn *Connection) AddReason(newReason string) {
func (conn *Connection) AddReason(reason string) {
if reason == "" {
return
}
conn.Lock()
defer conn.Unlock()
if conn.Reason != "" {
conn.Reason += " | "
}
conn.Reason += newReason
conn.Reason += reason
}
// GetConnectionByFirstPacket returns the matching connection from the internal storage.
@ -92,13 +98,25 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) {
if err != nil {
return nil, err
}
var domain string
// if INBOUND
// Incoming
if direction {
connection, ok := GetConnection(proc.Pid, "I")
switch netutils.ClassifyIP(pkt.GetIPHeader().Src) {
case HostLocal:
domain = IncomingHost
case LinkLocal, SiteLocal, LocalMulticast:
domain = IncomingLAN
case Global, GlobalMulticast:
domain = IncomingInternet
case Invalid:
domain = IncomingInvalid
}
connection, ok := GetConnection(proc.Pid, domain)
if !ok {
connection = &Connection{
Domain: "I",
Domain: domain,
Direction: Inbound,
process: proc,
Inspect: true,
@ -111,12 +129,26 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) {
// get domain
ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP())
// PeerToPeer
if err != nil {
// if no domain could be found, it must be a direct connection
connection, ok := GetConnection(proc.Pid, "D")
switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) {
case HostLocal:
domain = PeerHost
case LinkLocal, SiteLocal, LocalMulticast:
domain = PeerLAN
case Global, GlobalMulticast:
domain = PeerInternet
case Invalid:
domain = PeerInvalid
}
connection, ok := GetConnection(proc.Pid, domain)
if !ok {
connection = &Connection{
Domain: "D",
Domain: domain,
Direction: Outbound,
process: proc,
Inspect: true,
@ -127,6 +159,7 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) {
return connection, nil
}
// To Domain
// FIXME: how to handle multiple possible domains?
connection, ok := GetConnection(proc.Pid, ipinfo.Domains[0])
if !ok {

View file

@ -100,7 +100,7 @@ next:
if ip == nil {
return nil, errors.New(fmt.Sprintf("failed to parse IP: %s", peer.String()))
}
if !netutils.IPIsLocal(ip) {
if !netutils.IPIsLAN(ip) {
return ip, nil
}
continue next

View file

@ -80,8 +80,38 @@ func (link *Link) HandlePacket(pkt packet.Packet) {
pkt.Drop()
}
// Accept accepts the link and adds the given reason.
func (link *Link) Accept(reason string) {
link.AddReason(reason)
link.UpdateVerdict(ACCEPT)
}
// 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.Direction {
link.Drop(reason)
} else {
link.Block(reason)
}
}
// Block blocks the link and adds the given reason.
func (link *Link) Block(reason string) {
link.AddReason(reason)
link.UpdateVerdict(BLOCK)
}
// Drop drops the link and adds the given reason.
func (link *Link) Drop(reason string) {
link.AddReason(reason)
link.UpdateVerdict(DROP)
}
// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
func (link *Link) UpdateVerdict(newVerdict Verdict) {
link.Lock()
defer link.Unlock()
if newVerdict > link.Verdict {
link.Verdict = newVerdict
link.Save()
@ -89,14 +119,18 @@ func (link *Link) UpdateVerdict(newVerdict Verdict) {
}
// AddReason adds a human readable string as to why a certain verdict was set in regard to this link
func (link *Link) AddReason(newReason string) {
func (link *Link) AddReason(reason string) {
if reason == "" {
return
}
link.Lock()
defer link.Unlock()
if link.Reason != "" {
link.Reason += " | "
}
link.Reason += newReason
link.Reason += reason
}
// packetHandler sequentially handles queued packets

View file

@ -11,6 +11,7 @@ var (
cleanDomainRegex = regexp.MustCompile("^((xn--)?[a-z0-9-_]{0,61}[a-z0-9]{1,1}\\.)*(xn--)?([a-z0-9-]{1,61}|[a-z0-9-]{1,30}\\.[a-z]{2,}\\.)$")
)
// IsValidFqdn returns whether the given string is a valid fqdn.
func IsValidFqdn(fqdn string) bool {
return cleanDomainRegex.MatchString(fqdn)
}

View file

@ -4,95 +4,101 @@ package netutils
import "net"
// IP types
// IP classifications
const (
hostLocal int8 = iota
linkLocal
siteLocal
global
localMulticast
globalMulticast
invalid
HostLocal int8 = iota
LinkLocal
SiteLocal
Global
LocalMulticast
GlobalMulticast
Invalid
)
func classifyAddress(ip net.IP) int8 {
// ClassifyAddress returns the classification for the given IP address.
func ClassifyAddress(ip net.IP) int8 {
if ip4 := ip.To4(); ip4 != nil {
// IPv4
switch {
case ip4[0] == 127:
// 127.0.0.0/8
return hostLocal
return HostLocal
case ip4[0] == 169 && ip4[1] == 254:
// 169.254.0.0/16
return linkLocal
return LinkLocal
case ip4[0] == 10:
// 10.0.0.0/8
return siteLocal
return SiteLocal
case ip4[0] == 172 && ip4[1]&0xf0 == 16:
// 172.16.0.0/12
return siteLocal
return SiteLocal
case ip4[0] == 192 && ip4[1] == 168:
// 192.168.0.0/16
return siteLocal
return SiteLocal
case ip4[0] == 224:
// 224.0.0.0/8
return localMulticast
return LocalMulticast
case ip4[0] >= 225 && ip4[0] <= 239:
// 225.0.0.0/8 - 239.0.0.0/8
return globalMulticast
return GlobalMulticast
case ip4[0] >= 240:
// 240.0.0.0/8 - 255.0.0.0/8
return invalid
return Invalid
default:
return global
return Global
}
} else if len(ip) == net.IPv6len {
// IPv6
switch {
case ip.Equal(net.IPv6loopback):
return hostLocal
return HostLocal
case ip[0]&0xfe == 0xfc:
// fc00::/7
return siteLocal
return SiteLocal
case ip[0] == 0xfe && ip[1]&0xc0 == 0x80:
// fe80::/10
return linkLocal
return LinkLocal
case ip[0] == 0xff && ip[1] <= 0x05:
// ff00::/16 - ff05::/16
return localMulticast
return LocalMulticast
case ip[0] == 0xff:
// other ff00::/8
return globalMulticast
return GlobalMulticast
default:
return global
return Global
}
}
return invalid
return Invalid
}
// IPIsLocal returns true if the given IP is a site-local or link-local address
func IPIsLocal(ip net.IP) bool {
switch classifyAddress(ip) {
case siteLocal:
// IPIsLocalhost returns whether the IP refers to the host itself.
func IPIsLocalhost(ip net.IP) bool {
return ClassifyAddress(ip) == HostLocal
}
// IPIsLAN returns true if the given IP is a site-local or link-local address.
func IPIsLAN(ip net.IP) bool {
switch ClassifyAddress(ip) {
case SiteLocal:
return true
case linkLocal:
case LinkLocal:
return true
default:
return false
}
}
// IPIsGlobal returns true if the given IP is a global address
// IPIsGlobal returns true if the given IP is a global address.
func IPIsGlobal(ip net.IP) bool {
return classifyAddress(ip) == global
return ClassifyAddress(ip) == Global
}
// IPIsLinkLocal returns true if the given IP is a link-local address
// IPIsLinkLocal returns true if the given IP is a link-local address.
func IPIsLinkLocal(ip net.IP) bool {
return classifyAddress(ip) == linkLocal
return ClassifyAddress(ip) == LinkLocal
}
// IPIsSiteLocal returns true if the given IP is a site-local address
// IPIsSiteLocal returns true if the given IP is a site-local address.
func IPIsSiteLocal(ip net.IP) bool {
return classifyAddress(ip) == siteLocal
return ClassifyAddress(ip) == SiteLocal
}

View file

@ -6,14 +6,14 @@ import (
)
func TestIPClassification(t *testing.T) {
testClassification(t, net.IPv4(71, 87, 113, 211), global)
testClassification(t, net.IPv4(127, 0, 0, 1), hostLocal)
testClassification(t, net.IPv4(127, 255, 255, 1), hostLocal)
testClassification(t, net.IPv4(192, 168, 172, 24), siteLocal)
testClassification(t, net.IPv4(71, 87, 113, 211), Global)
testClassification(t, net.IPv4(127, 0, 0, 1), HostLocal)
testClassification(t, net.IPv4(127, 255, 255, 1), HostLocal)
testClassification(t, net.IPv4(192, 168, 172, 24), SiteLocal)
}
func testClassification(t *testing.T, ip net.IP, expectedClassification int8) {
c := classifyAddress(ip)
c := ClassifyAddress(ip)
if c != expectedClassification {
t.Errorf("%s is %s, expected %s", ip, classificationString(c), classificationString(expectedClassification))
}
@ -21,19 +21,19 @@ func testClassification(t *testing.T, ip net.IP, expectedClassification int8) {
func classificationString(c int8) string {
switch c {
case hostLocal:
case HostLocal:
return "hostLocal"
case linkLocal:
case LinkLocal:
return "linkLocal"
case siteLocal:
case SiteLocal:
return "siteLocal"
case global:
case Global:
return "global"
case localMulticast:
case LocalMulticast:
return "localMulticast"
case globalMulticast:
case GlobalMulticast:
return "globalMulticast"
case invalid:
case Invalid:
return "invalid"
default:
return "unknown"

View file

@ -9,7 +9,6 @@ type Verdict uint8
const (
// UNDECIDED is the default status of new connections
UNDECIDED Verdict = iota
CANTSAY
ACCEPT
BLOCK
DROP
@ -20,3 +19,15 @@ const (
Inbound = true
Outbound = false
)
// Non-Domain Connections
const (
IncomingHost = "IH"
IncomingLAN = "IL"
IncomingInternet = "II"
IncomingInvalid = "IX"
PeerHost = "PH"
PeerLAN = "PL"
PeerInternet = "PI"
PeerInvalid = "PX"
)

View file

@ -35,29 +35,17 @@ func updateActiveUserProfile(profile *Profile) {
}
}
func updateActiveGlobalProfile(profile *Profile) {
updateActiveProfile(1, profile)
}
func updateActiveStampProfile(profile *Profile) {
updateActiveProfile(2, profile)
}
func updateActiveFallbackProfile(profile *Profile) {
updateActiveProfile(3, profile)
}
func updateActiveProfile(setID int, profile *Profile) {
activeProfileSetsLock.RLock()
defer activeProfileSetsLock.RUnlock()
for _, activeSet := range activeProfileSets {
activeSet.Lock()
activeProfile := activeSet.profiles[setID]
activeProfile := activeSet.profiles[2]
if activeProfile != nil {
activeProfile.Lock()
if activeProfile.ID == profile.ID {
activeSet.profiles[setID] = profile
activeSet.profiles[2] = profile
}
activeProfile.Unlock()
}

View file

@ -1,5 +1,7 @@
package profile
import "time"
var (
fingerprintWeights = map[string]int{
"full_path": 2,
@ -10,41 +12,21 @@ var (
}
)
// Fingerprint links processes to profiles.
type Fingerprint struct {
OS string
Type string
Value string
Comment string
OS string
Type string
Value string
Comment string
LastUsed int64
}
// MatchesOS returns whether the Fingerprint is applicable for the current OS.
func (fp *Fingerprint) MatchesOS() bool {
return fp.OS == osIdentifier
}
//
// func (fp *Fingerprint) Equals(other *Fingerprint) bool {
// return fp.OS == other.OS &&
// fp.Type == other.Type &&
// fp.Value == other.Value
// }
//
// func (fp *Fingerprint) Check(type, value string) (weight int) {
// if fp.Match(fpType, value) {
// return GetFingerprintWeight(fpType)
// }
// return 0
// }
//
// func (fp *Fingerprint) Match(fpType, value string) (matches bool) {
// switch fp.Type {
// case "partial_path":
// return
// default:
// return fp.OS == osIdentifier &&
// fp.Type == fpType &&
// fp.Value == value
// }
//
// GetFingerprintWeight returns the weight of the given fingerprint type.
func GetFingerprintWeight(fpType string) (weight int) {
weight, ok := fingerprintWeights[fpType]
if ok {
@ -53,47 +35,14 @@ func GetFingerprintWeight(fpType string) (weight int) {
return 0
}
//
// func (p *Profile) GetApplicableFingerprints() (fingerprints []*Fingerprint) {
// for _, fp := range p.Fingerprints {
// if fp.OS == osIdentifier {
// fingerprints = append(fingerprints, fp)
// }
// }
// return
// }
//
// AddFingerprint adds the given fingerprint to the profile.
func (p *Profile) AddFingerprint(fp *Fingerprint) {
if fp.OS == "" {
fp.OS = osIdentifier
}
if fp.LastUsed == 0 {
fp.LastUsed = time.Now().Unix()
}
p.Fingerprints = append(p.Fingerprints, fp)
}
//
// func (p *Profile) GetApplicableFingerprintTypes() (types []string) {
// for _, fp := range p.Fingerprints {
// if fp.OS == osIdentifier && !utils.StringInSlice(types, fp.Type) {
// types = append(types, fp.Type)
// }
// }
// return
// }
//
// func (p *Profile) MatchFingerprints(fingerprints map[string]string) (score int) {
// for _, fp := range p.Fingerprints {
// if fp.OS == osIdentifier {
//
// }
// }
// return
// }
//
// func FindUserProfiles() {
//
// }
//
// func FindProfiles(path string) (*ProfileSet, error) {
//
// }

View file

@ -77,19 +77,6 @@ var (
}
)
// FlagsFromNames creates Flags from a comma seperated list of flagnames (e.g. "System,Strict,Secure")
// func FlagsFromNames(words []string) (*Flags, error) {
// var flags Flags
// for _, entry := range words {
// flag, ok := flagIDs[entry]
// if !ok {
// return nil, ErrFlagsParseFailed
// }
// flags = append(flags, flag)
// }
// return &flags, nil
// }
// Check checks if a flag is set at all and if it's active in the given security level.
func (flags Flags) Check(flag, level uint8) (active bool, ok bool) {
if flags == nil {

View file

@ -15,12 +15,14 @@ type ProfileIndex struct {
record.Base
sync.Mutex
ID string
UserProfiles []string
StampProfiles []string
}
func makeIndexRecordKey(id string) string {
return fmt.Sprintf("index:profiles/%s", base64.RawURLEncoding.EncodeToString([]byte(id)))
func makeIndexRecordKey(fpType, id string) string {
return fmt.Sprintf("index:profiles/%s:%s", fpType, base64.RawURLEncoding.EncodeToString([]byte(id)))
}
// NewIndex returns a new ProfileIndex.
@ -32,8 +34,8 @@ func NewIndex(id string) *ProfileIndex {
// AddUserProfile adds a User Profile to the index.
func (pi *ProfileIndex) AddUserProfile(identifier string) (changed bool) {
if !utils.StringInSlice(pi.UserProfiles, id) {
pi.UserProfiles = append(pi.UserProfiles, id)
if !utils.StringInSlice(pi.UserProfiles, identifier) {
pi.UserProfiles = append(pi.UserProfiles, identifier)
return true
}
return false
@ -41,8 +43,8 @@ func (pi *ProfileIndex) AddUserProfile(identifier string) (changed bool) {
// AddStampProfile adds a Stamp Profile to the index.
func (pi *ProfileIndex) AddStampProfile(identifier string) (changed bool) {
if !utils.StringInSlice(pi.StampProfiles, id) {
pi.StampProfiles = append(pi.StampProfiles, id)
if !utils.StringInSlice(pi.StampProfiles, identifier) {
pi.StampProfiles = append(pi.StampProfiles, identifier)
return true
}
return false
@ -59,8 +61,8 @@ func (pi *ProfileIndex) RemoveStampProfile(id string) {
}
// Get gets a ProfileIndex from the database.
func Get(id string) (*ProfileIndex, error) {
key := makeIndexRecordKey(id)
func Get(fpType, id string) (*ProfileIndex, error) {
key := makeIndexRecordKey(fpType, id)
r, err := indexDB.Get(key)
if err != nil {

View file

@ -1,8 +1,6 @@
package index
import (
"strings"
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/database/query"
"github.com/Safing/portbase/database/record"
@ -25,7 +23,7 @@ var (
)
func init() {
modules.Register("profile:index", nil, start, stop, "database")
modules.Register("profile:index", nil, start, stop, "profile", "database")
}
func start() (err error) {
@ -49,13 +47,17 @@ func indexer() {
case <-shutdownIndexer:
return
case r := <-indexSub.Feed:
if r == nil {
return
}
prof := ensureProfile(r)
if prof != nil {
for _, id := range prof.Identifiers {
if strings.HasPrefix(id, profile.IdentifierPrefix) {
for _, fp := range prof.Fingerprints {
if fp.MatchesOS() && fp.Type == "full_path" {
// get Profile and ensure identifier is set
pi, err := GetIndex(id)
pi, err := Get("full_path", fp.Value)
if err != nil {
if err == database.ErrNotFound {
pi = NewIndex(id)

View file

@ -1,3 +1,24 @@
package profile
.
import "github.com/Safing/portbase/modules"
var (
shutdownSignal = make(chan struct{})
)
func init() {
modules.Register("profile", nil, start, stop, "database")
}
func start() error {
err := initSpecialProfiles()
if err != nil {
return err
}
return initUpdateListener()
}
func stop() error {
close(shutdownSignal)
return nil
}

46
profile/ports_test.go Normal file
View file

@ -0,0 +1,46 @@
package profile
import (
"testing"
"time"
)
func TestPorts(t *testing.T) {
var ports Ports
ports = map[int16][]*Port{
6: []*Port{
&Port{ // SSH
Permit: true,
Created: time.Now().Unix(),
Start: 22,
End: 22,
},
},
-17: []*Port{
&Port{ // HTTP
Permit: false,
Created: time.Now().Unix(),
Start: 80,
End: 81,
},
},
93: []*Port{
&Port{ // HTTP
Permit: true,
Created: time.Now().Unix(),
Start: 93,
End: 93,
},
},
}
if ports.String() != "TCP:[permit:22], <UDP:[deny:80-81], 93:[permit:93]" {
t.Errorf("unexpected result: %s", ports.String())
}
var noPorts Ports
noPorts = map[int16][]*Port{}
if noPorts.String() != "None" {
t.Errorf("unexpected result: %s", ports.String())
}
}

View file

@ -37,8 +37,10 @@ type Profile struct {
Domains Domains
Ports Ports
StampProfileKey string
StampProfileAssigned int64
// User Profile Only
CoupledPath string `json:",omitempty"`
StampProfileKey string `json:",omitempty"`
StampProfileAssigned int64 `json:",omitempty"`
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
// Framework *Framework `json:",omitempty bson:",omitempty"`

View file

@ -42,6 +42,8 @@ func NewSet(user, stamp *Profile) *Set {
// Update gets the new global and default profile and updates the independence status. It must be called when reusing a profile set for a series of calls.
func (set *Set) Update(securityLevel uint8) {
set.Lock()
specialProfileLock.RLock()
defer specialProfileLock.RUnlock()
@ -58,15 +60,36 @@ func (set *Set) Update(securityLevel uint8) {
}
// update independence
set.Unlock()
if set.CheckFlag(Independent) {
set.Lock()
set.independent = true
set.Unlock()
} else {
set.Lock()
set.independent = false
set.Unlock()
}
}
// GetProfileMode returns the active profile mode.
func (set *Set) GetProfileMode() uint8 {
switch {
case set.CheckFlag(Whitelist):
return Whitelist
case set.CheckFlag(Prompt):
return Prompt
case set.CheckFlag(Blacklist):
return Blacklist
default:
return Whitelist
}
}
// CheckFlag returns whether a given flag is set.
func (set *Set) CheckFlag(flag uint8) (active bool) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles {
if i == 2 && set.independent {
@ -86,6 +109,8 @@ func (set *Set) CheckFlag(flag uint8) (active bool) {
// CheckDomain checks if the given domain is governed in any the lists of domains and returns whether it is permitted.
func (set *Set) CheckDomain(domain string) (permit, ok bool) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles {
if i == 2 && set.independent {
@ -105,6 +130,8 @@ func (set *Set) CheckDomain(domain string) (permit, ok bool) {
// CheckPort checks if the given protocol and port are governed in any the lists of ports and returns whether it is permitted.
func (set *Set) CheckPort(listen bool, protocol uint8, port uint16) (permit, ok bool) {
set.Lock()
defer set.Unlock()
signedProtocol := int16(protocol)
if listen {

View file

@ -1,17 +1,167 @@
package profile
import "testing"
import (
"testing"
"time"
"github.com/Safing/portmaster/status"
)
var (
testUserProfile *Profile
testStampProfile *Profile
)
func init() {
specialProfileLock.Lock()
defer specialProfileLock.Unlock()
globalProfile = makeDefaultGlobalProfile()
fallbackProfile = makeDefaultFallbackProfile()
testUserProfile = &Profile{
ID: "unit-test-user",
Name: "Unit Test User Profile",
SecurityLevel: status.SecurityLevelDynamic,
Flags: map[uint8]uint8{
Independent: status.SecurityLevelFortress,
},
Domains: map[string]*DomainDecision{
"example.com": &DomainDecision{
Permit: true,
Created: time.Now().Unix(),
IncludeSubdomains: false,
},
"bad.example.com": &DomainDecision{
Permit: false,
Created: time.Now().Unix(),
IncludeSubdomains: true,
},
},
Ports: map[int16][]*Port{
6: []*Port{
&Port{
Permit: true,
Created: time.Now().Unix(),
Start: 22000,
End: 22000,
},
},
},
}
testStampProfile = &Profile{
ID: "unit-test-stamp",
Name: "Unit Test Stamp Profile",
SecurityLevel: status.SecurityLevelFortress,
Flags: map[uint8]uint8{
Internet: status.SecurityLevelsAll,
},
Domains: map[string]*DomainDecision{
"bad2.example.com": &DomainDecision{
Permit: false,
Created: time.Now().Unix(),
IncludeSubdomains: true,
},
"good.bad.example.com": &DomainDecision{
Permit: true,
Created: time.Now().Unix(),
IncludeSubdomains: false,
},
},
Ports: map[int16][]*Port{
6: []*Port{
&Port{
Permit: false,
Created: time.Now().Unix(),
Start: 80,
End: 80,
},
},
-17: []*Port{
&Port{
Permit: true,
Created: time.Now().Unix(),
Start: 12345,
End: 12347,
},
},
},
}
}
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)
}
}
func testDomain(t *testing.T, set *Set, domain string, shouldBePermitted bool) {
permitted, ok := set.CheckDomain(domain)
if !ok {
t.Errorf("domain %s should be in test profile set", domain)
}
if permitted != shouldBePermitted {
t.Errorf("unexpected result: domain %s: permitted=%v, expected=%v", domain, permitted, shouldBePermitted)
}
}
func testUnregulatedDomain(t *testing.T, set *Set, domain string) {
_, ok := set.CheckDomain(domain)
if ok {
t.Errorf("domain %s should not be in test profile set", domain)
}
}
func testPort(t *testing.T, set *Set, listen bool, protocol uint8, port uint16, shouldBePermitted bool) {
permitted, ok := set.CheckPort(listen, protocol, port)
if !ok {
t.Errorf("port [%v %d %d] should be in test profile set", listen, protocol, port)
}
if permitted != shouldBePermitted {
t.Errorf("unexpected result: port [%v %d %d]: permitted=%v, expected=%v", listen, protocol, port, permitted, shouldBePermitted)
}
}
func testUnregulatedPort(t *testing.T, set *Set, listen bool, protocol uint8, port uint16) {
_, ok := set.CheckPort(listen, protocol, port)
if ok {
t.Errorf("port [%v %d %d] should not be in test profile set", listen, protocol, port)
}
}
func TestProfileSet(t *testing.T) {
// new := &Set{
// profiles: [4]*Profile{
// user, // Application
// nil, // Global
// stamp, // Stamp
// nil, // Default
// },
// }
// new.Update(status.SecurityLevelFortress)
set := NewSet(testUserProfile, testStampProfile)
set.Update(status.SecurityLevelDynamic)
testFlag(t, set, Whitelist, false)
testFlag(t, set, Internet, true)
testDomain(t, set, "example.com", true)
testDomain(t, set, "bad.example.com", false)
testDomain(t, set, "other.bad.example.com", false)
testDomain(t, set, "good.bad.example.com", false)
testDomain(t, set, "bad2.example.com", false)
testPort(t, set, false, 6, 443, true)
testPort(t, set, false, 6, 143, true)
testPort(t, set, false, 6, 22, true)
testPort(t, set, false, 6, 80, false)
testPort(t, set, false, 6, 80, false)
testPort(t, set, true, 17, 12345, true)
testPort(t, set, true, 17, 12346, true)
testPort(t, set, true, 17, 12347, true)
testUnregulatedDomain(t, set, "other.example.com")
testUnregulatedPort(t, set, false, 17, 53)
testUnregulatedPort(t, set, false, 17, 443)
testUnregulatedPort(t, set, true, 6, 443)
set.Update(status.SecurityLevelSecure)
testFlag(t, set, Internet, true)
set.Update(status.SecurityLevelFortress) // Independent!
testFlag(t, set, Internet, false)
testPort(t, set, false, 6, 80, true)
testUnregulatedDomain(t, set, "bad2.example.com")
testUnregulatedPort(t, set, true, 17, 12346)
}

View file

@ -1,7 +1,6 @@
package profile
import (
"fmt"
"strings"
"github.com/Safing/portbase/database"
@ -19,35 +18,41 @@ func initUpdateListener() error {
return nil
}
var (
slashedUserNamespace = fmt.Sprintf("/%s/", userNamespace)
slashedStampNamespace = fmt.Sprintf("/%s/", stampNamespace)
)
func updateListener(sub *database.Subscription) {
for r := range sub.Feed {
profile, err := ensureProfile(r)
if err != nil {
log.Errorf("profile: received update for special profile, but could not read: %s", err)
continue
}
for {
select {
case <-shutdownSignal:
return
case r := <-sub.Feed:
specialProfileLock.Lock()
switch profile.ID {
case "global":
globalProfile = profile
updateActiveGlobalProfile(profile)
case "fallback":
fallbackProfile = profile
updateActiveFallbackProfile(profile)
default:
switch {
case strings.HasPrefix(profile.Key(), makeProfileKey(userNamespace, "")):
updateActiveUserProfile(profile)
case strings.HasPrefix(profile.Key(), makeProfileKey(stampNamespace, "")):
updateActiveStampProfile(profile)
if r.Meta().IsDeleted() {
continue
}
profile, err := ensureProfile(r)
if err != nil {
log.Errorf("profile: received update for special profile, but could not read: %s", err)
continue
}
switch profile.ID {
case "global":
specialProfileLock.Lock()
globalProfile = profile
specialProfileLock.Unlock()
case "fallback":
specialProfileLock.Lock()
fallbackProfile = profile
specialProfileLock.Unlock()
default:
switch {
case strings.HasPrefix(profile.Key(), makeProfileKey(userNamespace, "")):
updateActiveUserProfile(profile)
case strings.HasPrefix(profile.Key(), makeProfileKey(stampNamespace, "")):
updateActiveStampProfile(profile)
}
}
}
specialProfileLock.Unlock()
}
}