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 package firewall
import ( import (
"net"
"os" "os"
"strings" "strings"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/Safing/portmaster/intel" "github.com/Safing/portmaster/intel"
"github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/network/packet" "github.com/Safing/portmaster/network/packet"
"github.com/Safing/portmaster/status"
"github.com/agext/levenshtein" "github.com/agext/levenshtein"
) )
@ -25,6 +24,7 @@ import (
// 4. DecideOnLink // 4. DecideOnLink
// is called when when the first packet of a link arrives only if connection has verdict UNDECIDED or CANTSAY // is called when when the first packet of a link arrives only if 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) { func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) {
// check: // check:
// Profile.DomainWhitelist // Profile.DomainWhitelist
@ -35,244 +35,227 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string)
// grant self // grant self
if connection.Process().Pid == os.Getpid() { if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection) log.Infof("firewall: granting own connection %s", connection)
connection.Accept() connection.Accept("")
return return
} }
// check if there is a profile // check if there is a profile
profileSet := connection.Process().ProfileSetSet profileSet := connection.Process().ProfileSet()
if profile == nil { if profileSet == nil {
log.Infof("firewall: no profile, denying connection %s", connection) log.Errorf("firewall: denying connection %s, no profile set", connection)
connection.AddReason("no profile") connection.Deny("no profile set")
connection.Block()
return return
} }
profileSet.Update(status.CurrentSecurityLevel())
// 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 // check for any network access
if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LocalNet) { if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) {
log.Infof("firewall: denying connection %s, profile denies Internet and local network access", connection) log.Infof("firewall: denying connection %s, accessing Internet or LAN not allowed", connection)
connection.Block() connection.Deny("accessing Internet or LAN not allowed")
return return
} }
// check domain whitelist/blacklist // check domain list
if len(profile.DomainWhitelist) > 0 { permitted, ok := profileSet.CheckDomain(fqdn)
matched := false if ok {
for _, entry := range profile.DomainWhitelist { if permitted {
if !strings.HasSuffix(entry, ".") { log.Infof("firewall: accepting connection %s, domain is whitelisted", connection, domainElement, processElement)
entry += "." connection.Accept("domain is whitelisted")
}
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
}
} else { } else {
if !profile.DomainWhitelistIsBlacklist { log.Infof("firewall: denying connection %s, domain is blacklisted", connection, domainElement, processElement)
log.Infof("firewall: denying connection %s, profile does not have %s in domain whitelist", connection, fqdn) connection.Deny("domain is blacklisted")
connection.AddReason("domain not in whitelist") }
connection.Block() return
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 { matchLoop:
// check: for _, domainElement = range domainElements {
// TODO: Profile.ClassificationBlacklist for _, pathElement := range pathElements {
// TODO: Profile.ClassificationWhitelist if levenshtein.Match(domainElement, pathElement, nil) > 0.5 {
// Profile.Flags matched = true
// - network specific: Strict processElement = pathElement
break matchLoop
// check if there is a profile }
profileSet := connection.Process().ProfileSet }
// FIXME: there should always be a profile if levenshtein.Match(domainElement, profile.Name, nil) > 0.5 {
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 {
matched = true 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 break matchLoop
} }
} }
if levenshtein.Match(domainElement, profile.Name, nil) > 0.5 {
matched = true if matched {
break matchLoop log.Infof("firewall: accepting connection %s, match to domain was found: %s ~= %s", connection, domainElement, processElement)
} connection.Accept("domain is related to process")
if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 {
matched = true
break matchLoop
} }
} }
if !matched {
log.Infof("firewall: denying connection %s, profile has declared Strict flag and no match to domain was found", connection) if connection.Verdict != network.ACCEPT {
connection.AddReason("domain does not relate to process") // TODO
connection.Block() log.Infof("firewall: accepting connection %s, domain permitted (prompting is not yet implemented)", connection, domainElement, processElement)
return rrCache 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) { // DecideOnConnectionAfterIntel makes a decision about a connection after the dns query is resolved and intel is gathered.
// check: func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache {
// Profile.Flags
// - process specific: System, Admin, User
// - network specific: Internet, LocalNet, Service, Directconnect
// grant self // grant self
if connection.Process().Pid == os.Getpid() { if connection.Process().Pid == os.Getpid() {
log.Infof("firewall: granting own connection %s", connection) 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 return
} }
// check if there is a profile // check if there is a profile
profileSet := connection.Process().ProfileSet profileSet := connection.Process().ProfileSet
if profile == nil { if profile == nil {
log.Infof("firewall: no profile, denying connection %s", connection) log.Errorf("firewall: denying connection %s, no profile set", connection)
connection.AddReason("no profile") connection.Deny("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()
return return
} }
profileSet.Update(status.CurrentSecurityLevel())
// check connection type
switch connection.Domain { switch connection.Domain {
case "I": case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid:
// check Service flag
if !profileSet.CheckFlag(profile.Service) { if !profileSet.CheckFlag(profile.Service) {
log.Infof("firewall: denying connection %s, profile does not declare service", connection) log.Infof("firewall: denying connection %s, not a service", connection)
connection.AddReason("not a service") if connection.Domain == IncomingHost {
connection.Drop() connection.Block("not a service")
} else {
connection.Drop("not a service")
}
return return
} }
// check if incoming connections are allowed on any port, but only if there no other restrictions case PeerLAN, PeerInternet, PeerInvalid: // Important: PeerHost is and should be missing!
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
if !profileSet.CheckFlag(profile.PeerToPeer) { if !profileSet.CheckFlag(profile.PeerToPeer) {
log.Infof("firewall: denying connection %s, profile does not declare direct connections", connection) log.Infof("firewall: denying connection %s, peer to peer connections (to an IP) not allowed", connection)
connection.AddReason("direct connections (without DNS) not allowed") connection.Deny("peer to peer connections (to an IP) not allowed")
connection.Drop()
return return
} }
} }
log.Infof("firewall: could not decide on connection %s, deciding on per-link basis", connection) // check network scope
connection.CantSay() 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) { func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet.Packet) {
// check: // check:
// Profile.Flags // Profile.Flags
@ -284,107 +267,44 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet
profileSet := connection.Process().ProfileSet profileSet := connection.Process().ProfileSet
if profile == nil { if profile == nil {
log.Infof("firewall: no profile, denying %s", link) log.Infof("firewall: no profile, denying %s", link)
link.AddReason("no profile") link.Block("no profile")
link.UpdateVerdict(network.BLOCK) 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 return
} }
// check LocalNet and Internet flags switch profileSet.GetProfileMode() {
var remoteIP net.IP case profile.Whitelist:
if connection.Direction { log.Infof("firewall: denying link %s: port %d is not whitelisted", link, remotePort)
remoteIP = pkt.GetIPHeader().Src link.Deny("port is not whitelisted")
} else { case profile.Prompt:
remoteIP = pkt.GetIPHeader().Dst log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort)
} link.Accept("port permitted (prompting is not yet implemented)")
if netutils.IPIsLocal(remoteIP) { case profile.Blacklist:
if !profileSet.CheckFlag(profile.LocalNet) { log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort)
log.Infof("firewall: dropping link %s, profile does not allow communication in the local network", link) link.Deny("port is not blacklisted")
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
}
} }
log.Infof("firewall: accepting link %s", link) log.Infof("firewall: accepting link %s", link)
link.UpdateVerdict(network.ACCEPT) link.Accept("")
} }

View file

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

View file

@ -15,6 +15,7 @@ import (
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
"github.com/Safing/portbase/log" "github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/Safing/portmaster/status" "github.com/Safing/portmaster/status"
) )
@ -76,26 +77,6 @@ func Resolve(fqdn string, qtype dns.Type, securityLevel uint8) *RRCache {
// timed := time.Now() // timed := time.Now()
// defer log.Tracef("intel: took %s to get resolve %s%s", time.Now().Sub(timed).String(), fqdn, qtype.String()) // 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 // check cache
rrCache, err := GetRRCache(fqdn, qtype) rrCache, err := GetRRCache(fqdn, qtype)
if err != nil { if err != nil {
@ -322,6 +303,14 @@ func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype
return nil, false return nil, false
} }
resolver.Initialized.SetToIf(false, true) resolver.Initialized.SetToIf(false, true)
// remove localhost entries, remove LAN entries if server is in global IP space.
if resolver.ServerIPScope == netutils.Global {
rrCache.FilterEntries(true, false, false)
} else {
rrCache.FilterEntries(true, true, false)
}
return rrCache, true return rrCache, true
} }
@ -368,11 +357,11 @@ func query(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) {
} }
new := &RRCache{ new := &RRCache{
Domain: fqdn, Domain: fqdn,
Question: qtype, Question: qtype,
Answer: reply.Answer, Answer: reply.Answer,
Ns: reply.Ns, Ns: reply.Ns,
Extra: reply.Extra, Extra: reply.Extra,
} }
// TODO: check if reply.Answer is valid // TODO: check if reply.Answer is valid

View file

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

View file

@ -3,9 +3,13 @@
package intel package intel
import ( import (
"fmt"
"net" "net"
"strings"
"time" "time"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/network/netutils"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -22,6 +26,7 @@ type RRCache struct {
updated int64 updated int64
servedFromCache bool servedFromCache bool
requestingNew bool requestingNew bool
Filtered bool
} }
// Clean sets all TTLs to 17 and sets cache expiry with specified minimum. // Clean sets all TTLs to 17 and sets cache expiry with specified minimum.
@ -77,6 +82,7 @@ func (m *RRCache) ToNameRecord() *NameRecord {
Domain: m.Domain, Domain: m.Domain,
Question: m.Question.String(), Question: m.Question.String(),
TTL: m.TTL, TTL: m.TTL,
Filtered: m.Filtered,
} }
// stringify RR entries // stringify RR entries
@ -130,6 +136,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) {
} }
} }
rrCache.Filtered = nameRecord.Filtered
rrCache.servedFromCache = true rrCache.servedFromCache = true
return rrCache, nil return rrCache, nil
} }
@ -146,19 +153,104 @@ func (m *RRCache) RequestingNew() bool {
// Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format. // Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format.
func (m *RRCache) Flags() string { func (m *RRCache) Flags() string {
switch { var s string
case m.servedFromCache && m.requestingNew: if m.servedFromCache {
return " [CR]" s += "C"
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 ""
} }
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. // IsNXDomain returnes whether the result is nxdomain.
func (m *RRCache) IsNXDomain() bool { func (m *RRCache) IsNXDomain() bool {
return len(m.Answer) == 0 return len(m.Answer) == 0
} }
// Duplicate returns a duplicate of the cache. slices are not copied, but referenced.
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" "github.com/Safing/portmaster/firewall"
) )
var (
localhostIPs []dns.RR
)
func init() { 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 { func start() error {
@ -49,7 +69,6 @@ func nxDomain(w dns.ResponseWriter, query *dns.Msg) {
func handleRequest(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: 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. // only process first question, that's how everyone does it.
question := query.Question[0] question := query.Question[0]
@ -84,6 +103,14 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) {
return return
} }
// handle request for localhost
if fqdn == "localhost." {
m := new(dns.Msg)
m.SetReply(query)
m.Answer = localhostIPs
w.WriteMsg(m)
}
// get remote address // get remote address
// start := time.Now() // start := time.Now()
rAddr, ok := w.RemoteAddr().(*net.UDPAddr) rAddr, ok := w.RemoteAddr().(*net.UDPAddr)

View file

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

View file

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

View file

@ -80,8 +80,38 @@ func (link *Link) HandlePacket(pkt packet.Packet) {
pkt.Drop() 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 // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts
func (link *Link) UpdateVerdict(newVerdict Verdict) { func (link *Link) UpdateVerdict(newVerdict Verdict) {
link.Lock()
defer link.Unlock()
if newVerdict > link.Verdict { if newVerdict > link.Verdict {
link.Verdict = newVerdict link.Verdict = newVerdict
link.Save() 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 // 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() link.Lock()
defer link.Unlock() defer link.Unlock()
if link.Reason != "" { if link.Reason != "" {
link.Reason += " | " link.Reason += " | "
} }
link.Reason += newReason link.Reason += reason
} }
// packetHandler sequentially handles queued packets // 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,}\\.)$") 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 { func IsValidFqdn(fqdn string) bool {
return cleanDomainRegex.MatchString(fqdn) return cleanDomainRegex.MatchString(fqdn)
} }

View file

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

View file

@ -9,7 +9,6 @@ type Verdict uint8
const ( const (
// UNDECIDED is the default status of new connections // UNDECIDED is the default status of new connections
UNDECIDED Verdict = iota UNDECIDED Verdict = iota
CANTSAY
ACCEPT ACCEPT
BLOCK BLOCK
DROP DROP
@ -20,3 +19,15 @@ const (
Inbound = true Inbound = true
Outbound = false 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) { func updateActiveStampProfile(profile *Profile) {
updateActiveProfile(2, profile)
}
func updateActiveFallbackProfile(profile *Profile) {
updateActiveProfile(3, profile)
}
func updateActiveProfile(setID int, profile *Profile) {
activeProfileSetsLock.RLock() activeProfileSetsLock.RLock()
defer activeProfileSetsLock.RUnlock() defer activeProfileSetsLock.RUnlock()
for _, activeSet := range activeProfileSets { for _, activeSet := range activeProfileSets {
activeSet.Lock() activeSet.Lock()
activeProfile := activeSet.profiles[setID] activeProfile := activeSet.profiles[2]
if activeProfile != nil { if activeProfile != nil {
activeProfile.Lock() activeProfile.Lock()
if activeProfile.ID == profile.ID { if activeProfile.ID == profile.ID {
activeSet.profiles[setID] = profile activeSet.profiles[2] = profile
} }
activeProfile.Unlock() activeProfile.Unlock()
} }

View file

@ -1,5 +1,7 @@
package profile package profile
import "time"
var ( var (
fingerprintWeights = map[string]int{ fingerprintWeights = map[string]int{
"full_path": 2, "full_path": 2,
@ -10,41 +12,21 @@ var (
} }
) )
// Fingerprint links processes to profiles.
type Fingerprint struct { type Fingerprint struct {
OS string OS string
Type string Type string
Value string Value string
Comment string Comment string
LastUsed int64
} }
// MatchesOS returns whether the Fingerprint is applicable for the current OS.
func (fp *Fingerprint) MatchesOS() bool { func (fp *Fingerprint) MatchesOS() bool {
return fp.OS == osIdentifier return fp.OS == osIdentifier
} }
// // GetFingerprintWeight returns the weight of the given fingerprint type.
// 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
// }
//
func GetFingerprintWeight(fpType string) (weight int) { func GetFingerprintWeight(fpType string) (weight int) {
weight, ok := fingerprintWeights[fpType] weight, ok := fingerprintWeights[fpType]
if ok { if ok {
@ -53,47 +35,14 @@ func GetFingerprintWeight(fpType string) (weight int) {
return 0 return 0
} }
// // AddFingerprint adds the given fingerprint to the profile.
// func (p *Profile) GetApplicableFingerprints() (fingerprints []*Fingerprint) {
// for _, fp := range p.Fingerprints {
// if fp.OS == osIdentifier {
// fingerprints = append(fingerprints, fp)
// }
// }
// return
// }
//
func (p *Profile) AddFingerprint(fp *Fingerprint) { func (p *Profile) AddFingerprint(fp *Fingerprint) {
if fp.OS == "" { if fp.OS == "" {
fp.OS = osIdentifier fp.OS = osIdentifier
} }
if fp.LastUsed == 0 {
fp.LastUsed = time.Now().Unix()
}
p.Fingerprints = append(p.Fingerprints, fp) 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. // 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) { func (flags Flags) Check(flag, level uint8) (active bool, ok bool) {
if flags == nil { if flags == nil {

View file

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

View file

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

View file

@ -1,3 +1,24 @@
package profile 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 Domains Domains
Ports Ports Ports Ports
StampProfileKey string // User Profile Only
StampProfileAssigned int64 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 // 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"` // 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. // 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) { func (set *Set) Update(securityLevel uint8) {
set.Lock()
specialProfileLock.RLock() specialProfileLock.RLock()
defer specialProfileLock.RUnlock() defer specialProfileLock.RUnlock()
@ -58,15 +60,36 @@ func (set *Set) Update(securityLevel uint8) {
} }
// update independence // update independence
set.Unlock()
if set.CheckFlag(Independent) { if set.CheckFlag(Independent) {
set.Lock()
set.independent = true set.independent = true
set.Unlock()
} else { } else {
set.Lock()
set.independent = false 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. // CheckFlag returns whether a given flag is set.
func (set *Set) CheckFlag(flag uint8) (active bool) { func (set *Set) CheckFlag(flag uint8) (active bool) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles { for i, profile := range set.profiles {
if i == 2 && set.independent { 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. // 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) { func (set *Set) CheckDomain(domain string) (permit, ok bool) {
set.Lock()
defer set.Unlock()
for i, profile := range set.profiles { for i, profile := range set.profiles {
if i == 2 && set.independent { 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. // 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) { func (set *Set) CheckPort(listen bool, protocol uint8, port uint16) (permit, ok bool) {
set.Lock()
defer set.Unlock()
signedProtocol := int16(protocol) signedProtocol := int16(protocol)
if listen { if listen {

View file

@ -1,17 +1,167 @@
package profile 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) { func TestProfileSet(t *testing.T) {
// new := &Set{ set := NewSet(testUserProfile, testStampProfile)
// profiles: [4]*Profile{
// user, // Application
// nil, // Global
// stamp, // Stamp
// nil, // Default
// },
// }
// new.Update(status.SecurityLevelFortress)
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 package profile
import ( import (
"fmt"
"strings" "strings"
"github.com/Safing/portbase/database" "github.com/Safing/portbase/database"
@ -19,35 +18,41 @@ func initUpdateListener() error {
return nil return nil
} }
var (
slashedUserNamespace = fmt.Sprintf("/%s/", userNamespace)
slashedStampNamespace = fmt.Sprintf("/%s/", stampNamespace)
)
func updateListener(sub *database.Subscription) { func updateListener(sub *database.Subscription) {
for r := range sub.Feed { for {
profile, err := ensureProfile(r) select {
if err != nil { case <-shutdownSignal:
log.Errorf("profile: received update for special profile, but could not read: %s", err) return
continue case r := <-sub.Feed:
}
specialProfileLock.Lock() if r.Meta().IsDeleted() {
switch profile.ID { continue
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)
} }
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()
} }
} }