mirror of
https://github.com/safing/portmaster
synced 2025-04-22 11:59:09 +00:00
Working on portmaster restructure
This commit is contained in:
parent
8fb21fd900
commit
8c11a35590
24 changed files with 850 additions and 554 deletions
firewall
intel
nameserver
network
profile
|
@ -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("")
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ type NameRecord struct {
|
|||
Ns []string
|
||||
Extra []string
|
||||
TTL int64
|
||||
Filtered bool
|
||||
}
|
||||
|
||||
func makeNameRecordKey(domain string, question string) string {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
110
intel/rrcache.go
110
intel/rrcache.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
//
|
||||
// }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
46
profile/ports_test.go
Normal 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())
|
||||
}
|
||||
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue