Merge pull request #183 from safing/feature/ui-revamp-ingest

Revamp profile and process handling, add optionKey to reason
This commit is contained in:
Patrick Pacher
2020-10-30 14:02:47 +01:00
committed by GitHub
34 changed files with 1200 additions and 810 deletions

View File

@@ -29,7 +29,7 @@ func registerConfig() error {
err := config.Register(&config.Option{
Name: "Development Mode",
Key: CfgDevModeKey,
Description: "In Development Mode security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
Description: "In Development Mode, security restrictions are lifted/softened to enable easier access to Portmaster for debugging and testing purposes.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelStable,
@@ -44,9 +44,9 @@ func registerConfig() error {
}
err = config.Register(&config.Option{
Name: "Use System Notifications",
Name: "Desktop Notifications",
Key: CfgUseSystemNotificationsKey,
Description: "Send notifications to your operating system's notification system. When this setting is turned off, notifications will only be visible in the Portmaster App. This affects both alerts from the Portmaster and questions from the Privacy Filter.",
Description: "In addition to showing notifications in the Portmaster App, also send them to the Desktop. This requires the Portmaster Notifier to be running.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelStable,

View File

@@ -45,9 +45,9 @@ func registerConfig() error {
permanentVerdicts = config.Concurrent.GetAsBool(CfgOptionPermanentVerdictsKey, true)
err = config.Register(&config.Option{
Name: "Ask with System Notifications",
Name: "Prompt Desktop Notifications",
Key: CfgOptionAskWithSystemNotificationsKey,
Description: `Ask about connections using your operating system's notification system. For this to be enabled, the setting "Use System Notifications" must enabled too. This only affects questions from the Privacy Filter, and does not affect alerts from the Portmaster.`,
Description: `In addition to showing prompt notifications in the Portmaster App, also send them to the Desktop. This requires the Portmaster Notifier to be running. Requires Desktop Notifications to be enabled.`,
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelExperimental,
@@ -66,9 +66,9 @@ func registerConfig() error {
}
err = config.Register(&config.Option{
Name: "Timeout for Ask Notifications",
Name: "Prompt Timeout",
Key: CfgOptionAskTimeoutKey,
Description: "Amount of time (in seconds) how long the Portmaster will wait for a response when prompting about a connection via a notification. Please note that system notifications might not respect this or have it's own limits.",
Description: "How long the Portmaster will wait for a reply to a prompt notification. Please note that Desktop Notifications might not respect this or have it's own limits.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelExperimental,
@@ -82,7 +82,7 @@ func registerConfig() error {
if err != nil {
return err
}
askTimeout = config.Concurrent.GetAsInt(CfgOptionAskTimeoutKey, 60)
askTimeout = config.Concurrent.GetAsInt(CfgOptionAskTimeoutKey, 15)
devMode = config.Concurrent.GetAsBool(core.CfgDevModeKey, false)
apiListenAddress = config.GetAsString(api.CfgDefaultListenAddressKey, "")

View File

@@ -17,13 +17,14 @@ import (
"github.com/safing/portmaster/resolver"
)
func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int) {
func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) ([]dns.RR, []string, int, string) {
goodEntries := make([]dns.RR, 0, len(entries))
filteredRecords := make([]string, 0, len(entries))
// keeps track of the number of valid and allowed
// A and AAAA records.
var allowedAddressRecords int
var interveningOptionKey string
for _, rr := range entries {
// get IP and classification
@@ -45,10 +46,12 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
case classification == netutils.HostLocal:
// No DNS should return localhost addresses
filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
continue
case scope == netutils.Global && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
// No global DNS should return LAN addresses
filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionRemoveOutOfScopeDNSKey
continue
}
}
@@ -58,12 +61,15 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
switch {
case p.BlockScopeInternet() && classification == netutils.Global:
filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionBlockScopeInternetKey
continue
case p.BlockScopeLAN() && (classification == netutils.SiteLocal || classification == netutils.LinkLocal):
filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionBlockScopeLANKey
continue
case p.BlockScopeLocal() && classification == netutils.HostLocal:
filteredRecords = append(filteredRecords, rr.String())
interveningOptionKey = profile.CfgOptionBlockScopeLocalKey
continue
}
@@ -75,7 +81,7 @@ func filterDNSSection(entries []dns.RR, p *profile.LayeredProfile, scope int8) (
goodEntries = append(goodEntries, rr)
}
return goodEntries, filteredRecords, allowedAddressRecords
return goodEntries, filteredRecords, allowedAddressRecords, interveningOptionKey
}
func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *resolver.RRCache {
@@ -97,18 +103,19 @@ func filterDNSResponse(conn *network.Connection, rrCache *resolver.RRCache) *res
var filteredRecords []string
var validIPs int
var interveningOptionKey string
rrCache.Answer, filteredRecords, validIPs = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope)
rrCache.Answer, filteredRecords, validIPs, interveningOptionKey = filterDNSSection(rrCache.Answer, p, rrCache.ServerScope)
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
// we don't count the valid IPs in the extra section
rrCache.Extra, filteredRecords, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope)
rrCache.Extra, filteredRecords, _, _ = filterDNSSection(rrCache.Extra, p, rrCache.ServerScope)
rrCache.FilteredEntries = append(rrCache.FilteredEntries, filteredRecords...)
if len(rrCache.FilteredEntries) > 0 {
rrCache.Filtered = true
if validIPs == 0 {
conn.Block("no addresses returned for this domain are permitted")
conn.Block("no addresses returned for this domain are permitted", interveningOptionKey)
// If all entries are filtered, this could mean that these are broken/bogus resource records.
if rrCache.Expired() {
@@ -151,12 +158,6 @@ func DecideOnResolvedDNS(
rrCache *resolver.RRCache,
) *resolver.RRCache {
// check profile
if checkProfileExists(ctx, conn, nil) {
// returns true if check triggered
return nil
}
// special grant for connectivity domains
if checkConnectivityDomain(ctx, conn, nil) {
// returns true if check triggered
@@ -186,14 +187,14 @@ func mayBlockCNAMEs(ctx context.Context, conn *network.Connection) bool {
result, reason := conn.Process().Profile().MatchEndpoint(ctx, conn.Entity)
if result == endpoints.Denied {
conn.BlockWithContext(reason.String(), reason.Context())
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
return true
}
if result == endpoints.NoMatch {
result, reason = conn.Process().Profile().MatchFilterLists(ctx, conn.Entity)
if result == endpoints.Denied {
conn.BlockWithContext(reason.String(), reason.Context())
conn.BlockWithContext(reason.String(), profile.CfgOptionFilterCNAMEKey, reason.Context())
return true
}
}

View File

@@ -24,9 +24,9 @@ func init() {
filterModule,
"config:filter/",
&config.Option{
Name: "Enable Privacy Filter",
Name: "Privacy Filter",
Key: CfgOptionEnableFilterKey,
Description: "Enable the Privacy Filter Subsystem to filter DNS queries and network requests.",
Description: "Enable the DNS and Network Filter.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelBeta,

View File

@@ -85,11 +85,11 @@ func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict
verdict = network.VerdictDrop
continueInspection = true
case BLOCK_CONN:
conn.SetVerdict(network.VerdictBlock, "", nil)
conn.SetVerdict(network.VerdictBlock, "", "", nil)
verdict = conn.Verdict
activeInspectors[key] = true
case DROP_CONN:
conn.SetVerdict(network.VerdictDrop, "", nil)
conn.SetVerdict(network.VerdictDrop, "", "", nil)
verdict = conn.Verdict
activeInspectors[key] = true
case STOP_INSPECTING:

View File

@@ -171,7 +171,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort())
if ps.isMe {
// approve
conn.Accept("internally approved")
conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true
// finish
conn.StopFirewallHandler()
@@ -191,7 +191,7 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) {
// check if filtering is enabled
if !filterEnabled() {
conn.Inspecting = false
conn.SetVerdict(network.VerdictAccept, "privacy filter disabled", nil)
conn.Accept("privacy filter disabled", noReasonOptionKey)
conn.StopFirewallHandler()
issueVerdict(conn, pkt, 0, true)
return

View File

@@ -70,7 +70,7 @@ func handleWindowsDNSCache() {
func notifyDisableDNSCache() {
(&notifications.Notification{
ID: "windows-disable-dns-cache",
EventID: "interception:windows-disable-dns-cache",
Message: "The Portmaster needs the Windows Service \"DNS Client\" (dnscache) to be disabled for best effectiveness.",
Type: notifications.Warning,
}).Save()
@@ -78,7 +78,7 @@ func notifyDisableDNSCache() {
func notifyRebootRequired() {
(&notifications.Notification{
ID: "windows-dnscache-reboot-required",
EventID: "interception:windows-dnscache-reboot-required",
Message: "Please restart your system to complete Portmaster integration.",
Type: notifications.Warning,
}).Save()

View File

@@ -36,44 +36,83 @@ import (
// 3. DecideOnConnection
// is called with the first packet of a network connection.
const noReasonOptionKey = ""
type deciderFn func(context.Context, *network.Connection, packet.Packet) bool
var deciders = []deciderFn{
checkPortmasterConnection,
checkSelfCommunication,
checkConnectionType,
checkConnectivityDomain,
checkConnectionScope,
checkEndpointLists,
checkBypassPrevention,
checkFilterLists,
dropInbound,
checkDomainHeuristics,
checkAutoPermitRelated,
}
// DecideOnConnection makes a decision about a connection.
// When called, the connection and profile is already locked.
func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) {
// update profiles and check if communication needs reevaluation
if conn.UpdateAndCheck() {
// Check if we have a process and profile.
layeredProfile := conn.Process().Profile()
if layeredProfile == nil {
conn.Deny("unknown process or profile", noReasonOptionKey)
return
}
// Check if the layered profile needs updating.
if layeredProfile.NeedsUpdate() {
// Update revision counter in connection.
conn.ProfileRevisionCounter = layeredProfile.Update()
conn.SaveWhenFinished()
// Reset verdict for connection.
log.Tracer(ctx).Infof("filter: re-evaluating verdict on %s", conn)
conn.Verdict = network.VerdictUndecided
// Reset entity if it exists.
if conn.Entity != nil {
conn.Entity.ResetLists()
}
}
var deciders = []func(context.Context, *network.Connection, packet.Packet) bool{
checkPortmasterConnection,
checkSelfCommunication,
checkProfileExists,
checkConnectionType,
checkConnectivityDomain,
checkConnectionScope,
checkEndpointLists,
checkBypassPrevention,
checkFilterLists,
checkInbound,
checkDomainHeuristics,
checkDefaultPermit,
checkAutoPermitRelated,
checkDefaultAction,
// Run all deciders and return if they came to a conclusion.
done, defaultAction := runDeciders(ctx, conn, pkt)
if done {
return
}
// Deciders did not conclude, use default action.
switch defaultAction {
case profile.DefaultActionPermit:
conn.Accept("default permit", profile.CfgOptionDefaultActionKey)
case profile.DefaultActionAsk:
prompt(ctx, conn, pkt)
default:
conn.Deny("default block", profile.CfgOptionDefaultActionKey)
}
}
func runDeciders(ctx context.Context, conn *network.Connection, pkt packet.Packet) (done bool, defaultAction uint8) {
layeredProfile := conn.Process().Profile()
// Read-lock the all the profiles.
layeredProfile.LockForUsage()
defer layeredProfile.UnlockForUsage()
// Go though all deciders, return if one sets an action.
for _, decider := range deciders {
if decider(ctx, conn, pkt) {
return
return true, profile.DefaultActionNotSet
}
}
// DefaultAction == DefaultActionBlock
conn.Deny("endpoint is not allowed (default=block)")
// Return the default action.
return false, layeredProfile.DefaultAction()
}
// checkPortmasterConnection allows all connection that originate from
@@ -82,7 +121,7 @@ func checkPortmasterConnection(ctx context.Context, conn *network.Connection, pk
// grant self
if conn.Process().Pid == os.Getpid() {
log.Tracer(ctx).Infof("filter: granting own connection %s", conn)
conn.Verdict = network.VerdictAccept
conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true
return true
}
@@ -115,7 +154,7 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, pkt p
if err != nil {
log.Tracer(ctx).Warningf("filter: failed to find load local peer process with PID %d: %s", otherPid, err)
} else if otherProcess.Pid == conn.Process().Pid {
conn.Accept("connection to self")
conn.Accept("process internal connection", noReasonOptionKey)
conn.Internal = true
return true
}
@@ -126,14 +165,6 @@ func checkSelfCommunication(ctx context.Context, conn *network.Connection, pkt p
return false
}
func checkProfileExists(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
if conn.Process().Profile() == nil {
conn.Block("unknown process or profile")
return true
}
return false
}
func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
var result endpoints.EPResult
var reason endpoints.Reason
@@ -142,17 +173,20 @@ func checkEndpointLists(ctx context.Context, conn *network.Connection, _ packet.
p := conn.Process().Profile()
// check endpoints list
var optionKey string
if conn.Inbound {
result, reason = p.MatchServiceEndpoint(ctx, conn.Entity)
optionKey = profile.CfgOptionServiceEndpointsKey
} else {
result, reason = p.MatchEndpoint(ctx, conn.Entity)
optionKey = profile.CfgOptionEndpointsKey
}
switch result {
case endpoints.Denied:
conn.DenyWithContext(reason.String(), reason.Context())
conn.DenyWithContext(reason.String(), optionKey, reason.Context())
return true
case endpoints.Permitted:
conn.AcceptWithContext(reason.String(), reason.Context())
conn.AcceptWithContext(reason.String(), optionKey, reason.Context())
return true
}
@@ -167,16 +201,16 @@ func checkConnectionType(ctx context.Context, conn *network.Connection, _ packet
case network.IncomingLAN, network.IncomingInternet, network.IncomingInvalid:
if p.BlockInbound() {
if conn.Scope == network.IncomingHost {
conn.Block("inbound connections blocked")
conn.Block("inbound connections blocked", profile.CfgOptionBlockInboundKey)
} else {
conn.Drop("inbound connections blocked")
conn.Drop("inbound connections blocked", profile.CfgOptionBlockInboundKey)
}
return true
}
case network.PeerInternet:
// BlockP2P only applies to connections to the Internet
if p.BlockP2P() {
conn.Block("direct connections (P2P) blocked")
conn.Block("direct connections (P2P) blocked", profile.CfgOptionBlockP2PKey)
return true
}
}
@@ -202,7 +236,7 @@ func checkConnectivityDomain(_ context.Context, conn *network.Connection, _ pack
case netenv.IsConnectivityDomain(conn.Entity.Domain):
// Special grant!
conn.Accept("special grant for connectivity domain during network bootstrap")
conn.Accept("special grant for connectivity domain during network bootstrap", noReasonOptionKey)
return true
default:
@@ -221,29 +255,29 @@ func checkConnectionScope(_ context.Context, conn *network.Connection, _ packet.
switch classification {
case netutils.Global, netutils.GlobalMulticast:
if p.BlockScopeInternet() {
conn.Deny("Internet access blocked") // Block Outbound / Drop Inbound
conn.Deny("Internet access blocked", profile.CfgOptionBlockScopeInternetKey) // Block Outbound / Drop Inbound
return true
}
case netutils.SiteLocal, netutils.LinkLocal, netutils.LocalMulticast:
if p.BlockScopeLAN() {
conn.Block("LAN access blocked") // Block Outbound / Drop Inbound
conn.Block("LAN access blocked", profile.CfgOptionBlockScopeLANKey) // Block Outbound / Drop Inbound
return true
}
case netutils.HostLocal:
if p.BlockScopeLocal() {
conn.Block("Localhost access blocked") // Block Outbound / Drop Inbound
conn.Block("Localhost access blocked", profile.CfgOptionBlockScopeLocalKey) // Block Outbound / Drop Inbound
return true
}
default: // netutils.Invalid
conn.Deny("invalid IP") // Block Outbound / Drop Inbound
conn.Deny("invalid IP", noReasonOptionKey) // Block Outbound / Drop Inbound
return true
}
} else if conn.Entity.Domain != "" {
// DNS Query
// DNS is expected to resolve to LAN or Internet addresses
// TODO: handle domains mapped to localhost
// This is a DNS Request.
// DNS is expected to resolve to LAN or Internet addresses.
// Localhost queries are immediately responded to by the nameserver.
if p.BlockScopeInternet() && p.BlockScopeLAN() {
conn.Block("Internet and LAN access blocked")
conn.Block("Internet and LAN access blocked", profile.CfgOptionBlockScopeInternetKey)
return true
}
}
@@ -256,10 +290,10 @@ func checkBypassPrevention(_ context.Context, conn *network.Connection, _ packet
result, reason, reasonCtx := PreventBypassing(conn)
switch result {
case endpoints.Denied:
conn.BlockWithContext("bypass prevention: "+reason, reasonCtx)
conn.BlockWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
return true
case endpoints.Permitted:
conn.AcceptWithContext("bypass prevention: "+reason, reasonCtx)
conn.AcceptWithContext("bypass prevention: "+reason, profile.CfgOptionPreventBypassingKey, reasonCtx)
return true
case endpoints.NoMatch:
}
@@ -274,7 +308,7 @@ func checkFilterLists(ctx context.Context, conn *network.Connection, pkt packet.
result, reason := p.MatchFilterLists(ctx, conn.Entity)
switch result {
case endpoints.Denied:
conn.DenyWithContext(reason.String(), reason.Context())
conn.DenyWithContext(reason.String(), profile.CfgOptionFilterListsKey, reason.Context())
return true
case endpoints.NoMatch:
// nothing to do
@@ -315,7 +349,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
domainToCheck,
score,
)
conn.Block("possible DGA domain commonly used by malware")
conn.Block("possible DGA domain commonly used by malware", profile.CfgOptionDomainHeuristicsKey)
return true
}
log.Tracer(ctx).Tracef("filter: LMS score of eTLD+1 %s is %.2f", etld1, score)
@@ -335,7 +369,7 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
domainToCheck,
score,
)
conn.Block("possible data tunnel for covert communication and protection bypassing")
conn.Block("possible data tunnel for covert communication and protection bypassing", profile.CfgOptionDomainHeuristicsKey)
return true
}
log.Tracer(ctx).Tracef("filter: LMS score of entire domain is %.2f", score)
@@ -344,20 +378,10 @@ func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ pack
return false
}
func checkInbound(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
func dropInbound(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
// implicit default=block for inbound
if conn.Inbound {
conn.Drop("endpoint is not allowed (incoming is always default=block)")
return true
}
return false
}
func checkDefaultPermit(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
// check default action
p := conn.Process().Profile()
if p.DefaultAction() == profile.DefaultActionPermit {
conn.Accept("endpoint is not blocked (default=permit)")
conn.Drop("incoming connection blocked by default", profile.CfgOptionServiceEndpointsKey)
return true
}
return false
@@ -365,22 +389,24 @@ func checkDefaultPermit(_ context.Context, conn *network.Connection, _ packet.Pa
func checkAutoPermitRelated(_ context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()
if !p.DisableAutoPermit() {
related, reason := checkRelation(conn)
if related {
conn.Accept(reason)
return true
}
}
return false
}
func checkDefaultAction(_ context.Context, conn *network.Connection, pkt packet.Packet) bool {
p := conn.Process().Profile()
if p.DefaultAction() == profile.DefaultActionAsk {
prompt(conn, pkt)
// Auto permit is disabled for default action permit.
if p.DefaultAction() == profile.DefaultActionPermit {
return false
}
// Check if auto permit is disabled.
if p.DisableAutoPermit() {
return false
}
// Check for relation to auto permit.
related, reason := checkRelation(conn)
if related {
conn.Accept(reason, profile.CfgOptionDisableAutoPermitKey)
return true
}
return false
}
@@ -426,7 +452,7 @@ matchLoop:
}
if related {
reason = fmt.Sprintf("domain is related to process: %s is related to %s", domainElement, processElement)
reason = fmt.Sprintf("auto permitted: domain is related to process: %s is related to %s", domainElement, processElement)
}
return related, reason
}

View File

@@ -1,15 +1,18 @@
package firewall
import (
"context"
"fmt"
"sync"
"time"
"github.com/safing/portmaster/profile/endpoints"
"github.com/safing/portbase/log"
"github.com/safing/portbase/notifications"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/profile"
"github.com/safing/portmaster/profile/endpoints"
)
const (
@@ -25,8 +28,47 @@ const (
denyServingIP = "deny-serving-ip"
)
func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // TODO
nTTL := time.Duration(askTimeout()) * time.Second
var (
promptNotificationCreation sync.Mutex
)
type promptData struct {
Entity *intel.Entity
Profile promptProfile
}
type promptProfile struct {
Source string
ID string
LinkedPath string
}
func prompt(ctx context.Context, conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // TODO
// Create notification.
n := createPrompt(ctx, conn, pkt)
// wait for response/timeout
select {
case promptResponse := <-n.Response():
switch promptResponse {
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
conn.Accept("permitted via prompt", profile.CfgOptionEndpointsKey)
default: // deny
conn.Deny("blocked via prompt", profile.CfgOptionEndpointsKey)
}
case <-time.After(1 * time.Second):
log.Tracer(ctx).Debugf("filter: continuing prompting async")
conn.Deny("prompting in progress", profile.CfgOptionDefaultActionKey)
case <-ctx.Done():
log.Tracer(ctx).Debugf("filter: aborting prompting because of shutdown")
conn.Drop("shutting down", noReasonOptionKey)
}
}
func createPrompt(ctx context.Context, conn *network.Connection, pkt packet.Packet) (n *notifications.Notification) {
expires := time.Now().Add(time.Duration(askTimeout()) * time.Second).Unix()
// first check if there is an existing notification for this.
// build notification ID
@@ -37,134 +79,154 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit //
default: // connection to domain
nID = fmt.Sprintf("filter:prompt-%d-%s", conn.Process().Pid, conn.Scope)
}
n := notifications.Get(nID)
saveResponse := true
// Only handle one notification at a time.
promptNotificationCreation.Lock()
defer promptNotificationCreation.Unlock()
n = notifications.Get(nID)
// If there already is a notification, just update the expiry.
if n != nil {
// update with new expiry
n.Update(time.Now().Add(nTTL).Unix())
// do not save response to profile
saveResponse = false
} else {
var (
msg string
actions []notifications.Action
n.Update(expires)
log.Tracer(ctx).Debugf("filter: updated existing prompt notification")
return
}
// Reference relevant data for save function
localProfile := conn.Process().Profile().LocalProfile()
entity := conn.Entity
// Create new notification.
n = &notifications.Notification{
EventID: nID,
Type: notifications.Prompt,
Title: "Connection Prompt",
Category: "Privacy Filter",
EventData: &promptData{
Entity: entity,
Profile: promptProfile{
Source: string(localProfile.Source),
ID: localProfile.ID,
LinkedPath: localProfile.LinkedPath,
},
},
Expires: expires,
}
// Set action function.
n.SetActionFunction(func(_ context.Context, n *notifications.Notification) error {
return saveResponse(
localProfile,
entity,
n.SelectedActionID,
)
})
// add message and actions
switch {
case conn.Inbound:
msg = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
actions = []notifications.Action{
{
ID: permitServingIP,
Text: "Permit",
},
{
ID: denyServingIP,
Text: "Deny",
},
}
case conn.Entity.Domain == "": // direct connection
msg = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
actions = []notifications.Action{
{
ID: permitIP,
Text: "Permit",
},
{
ID: denyIP,
Text: "Deny",
},
}
default: // connection to domain
if pkt != nil {
msg = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", conn.Process(), conn.Entity.Domain, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
} else {
msg = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain)
}
actions = []notifications.Action{
{
ID: permitDomainAll,
Text: "Permit all",
},
{
ID: permitDomainDistinct,
Text: "Permit",
},
{
ID: denyDomainDistinct,
Text: "Deny",
},
}
// add message and actions
switch {
case conn.Inbound:
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
n.AvailableActions = []*notifications.Action{
{
ID: permitServingIP,
Text: "Permit",
},
{
ID: denyServingIP,
Text: "Deny",
},
}
case conn.Entity.Domain == "": // direct connection
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
n.AvailableActions = []*notifications.Action{
{
ID: permitIP,
Text: "Permit",
},
{
ID: denyIP,
Text: "Deny",
},
}
default: // connection to domain
n.Message = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain)
n.AvailableActions = []*notifications.Action{
{
ID: permitDomainAll,
Text: "Permit all",
},
{
ID: permitDomainDistinct,
Text: "Permit",
},
{
ID: denyDomainDistinct,
Text: "Deny",
},
}
n = notifications.NotifyPrompt(nID, msg, actions...)
}
// wait for response/timeout
select {
case promptResponse := <-n.Response():
switch promptResponse {
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
conn.Accept("permitted by user")
default: // deny
conn.Deny("denied by user")
}
n.Save()
log.Tracer(ctx).Debugf("filter: sent prompt notification")
// end here if we won't save the response to the profile
if !saveResponse {
return
}
// get profile
p := conn.Process().Profile()
var ep endpoints.Endpoint
switch promptResponse {
case permitDomainAll:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: "." + conn.Entity.Domain,
}
case permitDomainDistinct:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: conn.Entity.Domain,
}
case denyDomainAll:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false},
Domain: "." + conn.Entity.Domain,
}
case denyDomainDistinct:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false},
Domain: conn.Entity.Domain,
}
case permitIP, permitServingIP:
ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: true},
IP: conn.Entity.IP,
}
case denyIP, denyServingIP:
ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: false},
IP: conn.Entity.IP,
}
default:
log.Warningf("filter: unknown prompt response: %s", promptResponse)
return
}
switch promptResponse {
case permitServingIP, denyServingIP:
p.AddServiceEndpoint(ep.String())
default:
p.AddEndpoint(ep.String())
}
case <-n.Expired():
conn.Deny("no response to prompt")
}
return n
}
func saveResponse(p *profile.Profile, entity *intel.Entity, promptResponse string) error {
// Update the profile if necessary.
if p.IsOutdated() {
var err error
p, _, err = profile.GetProfile(p.Source, p.ID, p.LinkedPath)
if err != nil {
return err
}
}
var ep endpoints.Endpoint
switch promptResponse {
case permitDomainAll:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true},
OriginalValue: "." + entity.Domain,
}
case permitDomainDistinct:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true},
OriginalValue: entity.Domain,
}
case denyDomainAll:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false},
OriginalValue: "." + entity.Domain,
}
case denyDomainDistinct:
ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false},
OriginalValue: entity.Domain,
}
case permitIP, permitServingIP:
ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: true},
IP: entity.IP,
}
case denyIP, denyServingIP:
ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: false},
IP: entity.IP,
}
default:
return fmt.Errorf("unknown prompt response: %s", promptResponse)
}
switch promptResponse {
case permitServingIP, denyServingIP:
p.AddServiceEndpoint(ep.String())
log.Infof("filter: added incoming rule to profile %s: %q", p, ep.String())
default:
p.AddEndpoint(ep.String())
log.Infof("filter: added outgoing rule to profile %s: %q", p, ep.String())
}
return nil
}

View File

@@ -197,11 +197,11 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
// A reason for this might be that the request is sink-holed to a forced
// IP address in which case we "accept" it, but let the firewall handle
// the resolving as it wishes.
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
// Save the request as open, as we don't know if there will be a connection or not.
network.SaveOpenDNSRequest(conn)
tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason)
tracer.Infof("nameserver: handing over request for %s to special filter responder: %s", q.ID(), conn.Reason.Msg)
return reply(responder)
}
@@ -243,11 +243,11 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
rrCache = firewall.DecideOnResolvedDNS(ctx, conn, q, rrCache)
if rrCache == nil {
// Check again if there is a responder from the firewall.
if responder, ok := conn.ReasonContext.(nsutil.Responder); ok {
if responder, ok := conn.Reason.Context.(nsutil.Responder); ok {
// Save the request as open, as we don't know if there will be a connection or not.
network.SaveOpenDNSRequest(conn)
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason)
tracer.Infof("nameserver: handing over request for %s to filter responder: %s", q.ID(), conn.Reason.Msg)
return reply(responder)
}

View File

@@ -47,10 +47,16 @@ func checkForConflictingService() error {
// wait for a short duration for the other service to shut down
time.Sleep(10 * time.Millisecond)
notifications.NotifyInfo(
"namserver-stopped-conflicting-service",
fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
)
notifications.Notify(&notifications.Notification{
EventID: "namserver:stopped-conflicting-service",
Type: notifications.Info,
Title: "Conflicting DNS Service",
Category: "Secure DNS",
Message: fmt.Sprintf(
"The Portmaster stopped a conflicting name service (pid %d) to gain required system integration.",
pid,
),
})
// restart via service-worker logic
return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid)

View File

@@ -213,16 +213,6 @@ func setCaptivePortal(portalURL *url.URL) {
return
}
// notify
cleanUpPortalNotification()
defer func() {
// TODO: add "open" button
captivePortalNotification = notifications.NotifyInfo(
"netenv:captive-portal:"+captivePortal.Domain,
"Portmaster detected a captive portal at "+captivePortal.Domain,
)
}()
// set
captivePortal = &CaptivePortal{
URL: portalURL.String(),
@@ -234,6 +224,20 @@ func setCaptivePortal(portalURL *url.URL) {
} else {
captivePortal.Domain = portalURL.Hostname()
}
// notify
cleanUpPortalNotification()
captivePortalNotification = notifications.Notify(&notifications.Notification{
EventID: "netenv:captive-portal",
Type: notifications.Info,
Title: "Captive Portal",
Category: "Core",
Message: fmt.Sprintf(
"Portmaster detected a captive portal at %s",
captivePortal.Domain,
),
EventData: captivePortal,
})
}
func cleanUpPortalNotification() {

View File

@@ -83,12 +83,10 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// The verdict may change so any access to it must be guarded by the
// connection lock.
Verdict Verdict
// Reason is a human readable description justifying the set verdict.
// Reason holds information justifying the verdict, as well as additional
// information about the reason.
// Access to Reason must be guarded by the connection lock.
Reason string
// ReasonContext may holds additional reason-specific information and
// any access must be guarded by the connection lock.
ReasonContext interface{}
Reason Reason
// Started holds the number of seconds in UNIX epoch time at which
// the connection has been initated and first seen by the portmaster.
// Staretd is only every set when creating a new connection object
@@ -96,7 +94,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
Started int64
// Ended is set to the number of seconds in UNIX epoch time at which
// the connection is considered terminated. Ended may be set at any
// time so access must be guarded by the conneciton lock.
// time so access must be guarded by the connection lock.
Ended int64
// VerdictPermanent is set to true if the final verdict is permanent
// and the connection has been (or will be) handed back to the kernel.
@@ -121,7 +119,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// points and access to it must be guarded by the connection lock.
Internal bool
// process holds a reference to the actor process. That is, the
// process instance that initated the conneciton.
// process instance that initated the connection.
process *process.Process
// pkgQueue is used to serialize packet handling for a single
// connection and is served by the connections packetHandler.
@@ -141,10 +139,26 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// inspectorData holds additional meta data for the inspectors.
// using the inspectors index as a map key.
inspectorData map[uint8]interface{}
// profileRevisionCounter is used to track changes to the process
// ProfileRevisionCounter is used to track changes to the process
// profile and required for correct re-evaluation of a connections
// verdict.
profileRevisionCounter uint64
ProfileRevisionCounter uint64
}
// Reason holds information justifying a verdict, as well as additional
// information about the reason.
type Reason struct {
// Msg is a human readable description of the reason.
Msg string
// OptionKey is the configuration option key of the setting that
// was responsible for the verdict.
OptionKey string
// Profile is the database key of the profile that held the setting
// that was responsible for the verdict.
Profile string
// ReasonContext may hold additional reason-specific information and
// any access must be guarded by the connection lock.
Context interface{}
}
func getProcessContext(proc *process.Process) ProcessContext {
@@ -290,7 +304,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
Entity: entity,
// meta
Started: time.Now().Unix(),
profileRevisionCounter: proc.Profile().RevisionCnt(),
ProfileRevisionCounter: proc.Profile().RevisionCnt(),
}
}
@@ -300,73 +314,77 @@ func GetConnection(id string) (*Connection, bool) {
}
// AcceptWithContext accepts the connection.
func (conn *Connection) AcceptWithContext(reason string, ctx interface{}) {
if !conn.SetVerdict(VerdictAccept, reason, ctx) {
func (conn *Connection) AcceptWithContext(reason, reasonOptionKey string, ctx interface{}) {
if !conn.SetVerdict(VerdictAccept, reason, reasonOptionKey, ctx) {
log.Warningf("filter: tried to accept %s, but current verdict is %s", conn, conn.Verdict)
}
}
// Accept is like AcceptWithContext but only accepts a reason.
func (conn *Connection) Accept(reason string) {
conn.AcceptWithContext(reason, nil)
func (conn *Connection) Accept(reason, reasonOptionKey string) {
conn.AcceptWithContext(reason, reasonOptionKey, nil)
}
// BlockWithContext blocks the connection.
func (conn *Connection) BlockWithContext(reason string, ctx interface{}) {
if !conn.SetVerdict(VerdictBlock, reason, ctx) {
func (conn *Connection) BlockWithContext(reason, reasonOptionKey string, ctx interface{}) {
if !conn.SetVerdict(VerdictBlock, reason, reasonOptionKey, ctx) {
log.Warningf("filter: tried to block %s, but current verdict is %s", conn, conn.Verdict)
}
}
// Block is like BlockWithContext but does only accepts a reason.
func (conn *Connection) Block(reason string) {
conn.BlockWithContext(reason, nil)
func (conn *Connection) Block(reason, reasonOptionKey string) {
conn.BlockWithContext(reason, reasonOptionKey, nil)
}
// DropWithContext drops the connection.
func (conn *Connection) DropWithContext(reason string, ctx interface{}) {
if !conn.SetVerdict(VerdictDrop, reason, ctx) {
func (conn *Connection) DropWithContext(reason, reasonOptionKey string, ctx interface{}) {
if !conn.SetVerdict(VerdictDrop, reason, reasonOptionKey, ctx) {
log.Warningf("filter: tried to drop %s, but current verdict is %s", conn, conn.Verdict)
}
}
// Drop is like DropWithContext but does only accepts a reason.
func (conn *Connection) Drop(reason string) {
conn.DropWithContext(reason, nil)
func (conn *Connection) Drop(reason, reasonOptionKey string) {
conn.DropWithContext(reason, reasonOptionKey, nil)
}
// DenyWithContext blocks or drops the link depending on the connection direction.
func (conn *Connection) DenyWithContext(reason string, ctx interface{}) {
func (conn *Connection) DenyWithContext(reason, reasonOptionKey string, ctx interface{}) {
if conn.Inbound {
conn.DropWithContext(reason, ctx)
conn.DropWithContext(reason, reasonOptionKey, ctx)
} else {
conn.BlockWithContext(reason, ctx)
conn.BlockWithContext(reason, reasonOptionKey, ctx)
}
}
// Deny is like DenyWithContext but only accepts a reason.
func (conn *Connection) Deny(reason string) {
conn.DenyWithContext(reason, nil)
func (conn *Connection) Deny(reason, reasonOptionKey string) {
conn.DenyWithContext(reason, reasonOptionKey, nil)
}
// FailedWithContext marks the connection with VerdictFailed and stores the reason.
func (conn *Connection) FailedWithContext(reason string, ctx interface{}) {
if !conn.SetVerdict(VerdictFailed, reason, ctx) {
func (conn *Connection) FailedWithContext(reason, reasonOptionKey string, ctx interface{}) {
if !conn.SetVerdict(VerdictFailed, reason, reasonOptionKey, ctx) {
log.Warningf("filter: tried to drop %s due to error but current verdict is %s", conn, conn.Verdict)
}
}
// Failed is like FailedWithContext but only accepts a string.
func (conn *Connection) Failed(reason string) {
conn.FailedWithContext(reason, nil)
func (conn *Connection) Failed(reason, reasonOptionKey string) {
conn.FailedWithContext(reason, reasonOptionKey, nil)
}
// SetVerdict sets a new verdict for the connection, making sure it does not interfere with previous verdicts.
func (conn *Connection) SetVerdict(newVerdict Verdict, reason string, reasonCtx interface{}) (ok bool) {
func (conn *Connection) SetVerdict(newVerdict Verdict, reason, reasonOptionKey string, reasonCtx interface{}) (ok bool) {
if newVerdict >= conn.Verdict {
conn.Verdict = newVerdict
conn.Reason = reason
conn.ReasonContext = reasonCtx
conn.Reason.Msg = reason
conn.Reason.Context = reasonCtx
if reasonOptionKey != "" && conn.Process() != nil {
conn.Reason.OptionKey = reasonOptionKey
conn.Reason.Profile = conn.Process().Profile().GetProfileSource(conn.Reason.OptionKey)
}
return true
}
return false
@@ -424,21 +442,6 @@ func (conn *Connection) delete() {
dbController.PushUpdate(conn)
}
// UpdateAndCheck updates profiles and checks whether a reevaluation is needed.
func (conn *Connection) UpdateAndCheck() (needsReevaluation bool) {
p := conn.process.Profile()
if p == nil {
return false
}
revCnt := p.Update()
if conn.profileRevisionCounter != revCnt {
conn.profileRevisionCounter = revCnt
needsReevaluation = true
}
return
}
// SetFirewallHandler sets the firewall handler for this link, and starts a
// worker to handle the packets.
func (conn *Connection) SetFirewallHandler(handler FirewallHandler) {
@@ -490,7 +493,7 @@ func (conn *Connection) packetHandler() {
defaultFirewallHandler(conn, pkt)
}
// log verdict
log.Tracer(pkt.Ctx()).Infof("filter: connection %s %s: %s", conn, conn.Verdict.Verb(), conn.Reason)
log.Tracer(pkt.Ctx()).Infof("filter: connection %s %s: %s", conn, conn.Verdict.Verb(), conn.Reason.Msg)
// save does not touch any changing data
// must not be locked, will deadlock with cleaner functions

View File

@@ -124,15 +124,15 @@ func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns
}
// Create resource record with verdict and reason.
rr, err := nsutil.MakeMessageRecord(level, fmt.Sprintf("%s: %s", conn.Verdict.Verb(), conn.Reason))
rr, err := nsutil.MakeMessageRecord(level, fmt.Sprintf("%s: %s", conn.Verdict.Verb(), conn.Reason.Msg))
if err != nil {
log.Tracer(ctx).Warningf("filter: failed to add informational record to reply: %s", err)
return nil
}
extra := []dns.RR{rr}
// Add additional records from ReasonContext.
if rrProvider, ok := conn.ReasonContext.(nsutil.RRProvider); ok {
// Add additional records from Reason.Context.
if rrProvider, ok := conn.Reason.Context.(nsutil.RRProvider); ok {
rrs := rrProvider.GetExtraRRs(ctx, request)
extra = append(extra, rrs...)
}

View File

@@ -14,7 +14,7 @@ func registerConfiguration() error {
// Enable Process Detection
// This should be always enabled. Provided as an option to disable in case there are severe problems on a system, or for debugging.
err := config.Register(&config.Option{
Name: "Enable Process Detection",
Name: "Process Detection",
Key: CfgOptionEnableProcessDetectionKey,
Description: "This option enables the attribution of network traffic to processes. This should be always enabled, and effectively disables app profiles if disabled.",
OptType: config.OptTypeBool,

View File

@@ -106,32 +106,34 @@ func CleanProcessStorage(activePIDs map[int]struct{}) {
// clean primary processes
for _, p := range processesCopy {
p.Lock()
// The PID of a process does not change.
_, active := activePIDs[p.Pid]
switch {
case p.Pid == UnidentifiedProcessID:
// internal
case p.Pid == SystemProcessID:
// internal
case active:
// process in system process table or recently seen on the network
default:
// delete now or soon
switch {
case p.LastSeen == 0:
// add last
p.LastSeen = time.Now().Unix()
case p.LastSeen > threshold:
// within keep period
default:
// delete now
log.Tracef("process.clean: deleted %s", p.DatabaseKey())
go p.Delete()
}
// Check if this is a special process.
if p.Pid == UnidentifiedProcessID || p.Pid == SystemProcessID {
p.profile.MarkStillActive()
continue
}
p.Unlock()
// Check if process is active.
_, active := activePIDs[p.Pid]
if active {
p.profile.MarkStillActive()
continue
}
// Process is inactive, start deletion process
lastSeen := p.GetLastSeen()
switch {
case lastSeen == 0:
// add last seen timestamp
p.SetLastSeen(time.Now().Unix())
case lastSeen > threshold:
// within keep period
default:
// delete now
p.Delete()
log.Tracef("process: cleaned %s", p.DatabaseKey())
}
}
}

View File

@@ -30,10 +30,14 @@ func GetProcessByConnection(ctx context.Context, pktInfo *packet.Info) (process
return nil, connInbound, err
}
err = process.GetProfile(ctx)
changed, err := process.GetProfile(ctx)
if err != nil {
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
}
if changed {
process.Save()
}
return process, connInbound, nil
}

View File

@@ -30,40 +30,56 @@ type Process struct {
record.Base
sync.Mutex
// Constant attributes.
Name string
UserID int
UserName string
UserHome string
Pid int
ParentPid int
Path string
ExecName string
Cwd string
CmdLine string
FirstArg string
ExecName string
ExecHashes map[string]string
// ExecOwner ...
// ExecSignature ...
LocalProfileKey string
profile *profile.LayeredProfile
Name string
Icon string
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache.
// Mutable attributes.
FirstSeen int64
LastSeen int64
Virtual bool // This process is either merged into another process or is not needed.
Error string // Cache errors
Virtual bool // This process is either merged into another process or is not needed.
Error string // Cache errors
ExecHashes map[string]string
}
// Profile returns the assigned layered profile.
func (p *Process) Profile() *profile.LayeredProfile {
if p == nil {
return nil
}
return p.profile
}
// GetLastSeen returns the unix timestamp when the process was last seen.
func (p *Process) GetLastSeen() int64 {
p.Lock()
defer p.Unlock()
return p.profile
return p.LastSeen
}
// SetLastSeen sets the unix timestamp when the process was last seen.
func (p *Process) SetLastSeen(lastSeen int64) {
p.Lock()
defer p.Unlock()
p.LastSeen = lastSeen
}
// Strings returns a string representation of process.
@@ -72,8 +88,6 @@ func (p *Process) String() string {
return "?"
}
p.Lock()
defer p.Unlock()
return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid)
}

View File

@@ -8,35 +8,58 @@ import (
)
// GetProfile finds and assigns a profile set to the process.
func (p *Process) GetProfile(ctx context.Context) error {
func (p *Process) GetProfile(ctx context.Context) (changed bool, err error) {
p.Lock()
defer p.Unlock()
// only find profiles if not already done.
if p.profile != nil {
log.Tracer(ctx).Trace("process: profile already loaded")
// mark profile as used
// Mark profile as used.
p.profile.MarkUsed()
return nil
return false, nil
}
log.Tracer(ctx).Trace("process: loading profile")
// get profile
localProfile, new, err := profile.FindOrCreateLocalProfileByPath(p.Path)
if err != nil {
return err
// Check if we need a special profile.
profileID := ""
switch p.Pid {
case UnidentifiedProcessID:
profileID = profile.UnidentifiedProfileID
case SystemProcessID:
profileID = profile.SystemProfileID
}
// add more information if new
// Get the (linked) local profile.
localProfile, new, err := profile.GetProfile(profile.SourceLocal, profileID, p.Path)
if err != nil {
return false, err
}
// If the local profile is new, add some information from the process.
if new {
localProfile.Name = p.ExecName
// Special profiles will only have a name, but not an ExecName.
if localProfile.Name == "" {
localProfile.Name = p.Name
}
}
// mark profile as used
localProfile.MarkUsed()
// Mark profile as used.
profileChanged := localProfile.MarkUsed()
// Save the profile if we changed something.
if new || profileChanged {
err := localProfile.Save()
if err != nil {
log.Warningf("process: failed to save profile %s: %s", localProfile.ScopedID(), err)
}
}
// Assign profile to process.
p.LocalProfileKey = localProfile.Key()
p.profile = profile.NewLayeredProfile(localProfile)
p.profile = localProfile.LayeredProfile()
go p.Save()
return nil
return true, nil
}

View File

@@ -2,10 +2,11 @@ package process
import (
"context"
"strconv"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
"golang.org/x/sync/singleflight"
)
// Special Process IDs
@@ -32,53 +33,41 @@ var (
ParentPid: SystemProcessID,
Name: "Operating System",
}
getSpecialProcessSingleInflight singleflight.Group
)
// GetUnidentifiedProcess returns the special process assigned to unidentified processes.
func GetUnidentifiedProcess(ctx context.Context) *Process {
return getSpecialProcess(ctx, UnidentifiedProcessID, unidentifiedProcess, profile.GetUnidentifiedProfile)
return getSpecialProcess(ctx, UnidentifiedProcessID, unidentifiedProcess)
}
// GetSystemProcess returns the special process used for the Kernel.
func GetSystemProcess(ctx context.Context) *Process {
return getSpecialProcess(ctx, SystemProcessID, systemProcess, profile.GetSystemProfile)
return getSpecialProcess(ctx, SystemProcessID, systemProcess)
}
func getSpecialProcess(ctx context.Context, pid int, template *Process, getProfile func() *profile.Profile) *Process {
// check storage
p, ok := GetProcessFromStorage(pid)
if ok {
return p
}
func getSpecialProcess(ctx context.Context, pid int, template *Process) *Process {
p, _, _ := getSpecialProcessSingleInflight.Do(strconv.Itoa(pid), func() (interface{}, error) {
// Check if we have already loaded the special process.
process, ok := GetProcessFromStorage(pid)
if ok {
return process, nil
}
// assign template
p = template
// Create new process from template
process = template
process.FirstSeen = time.Now().Unix()
p.Lock()
defer p.Unlock()
// Get profile.
_, err := process.GetProfile(ctx)
if err != nil {
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)
}
if p.FirstSeen == 0 {
p.FirstSeen = time.Now().Unix()
}
// only find profiles if not already done.
if p.profile != nil {
log.Tracer(ctx).Trace("process: special profile already loaded")
// mark profile as used
p.profile.MarkUsed()
return p
}
log.Tracer(ctx).Trace("process: loading special profile")
// get profile
localProfile := getProfile()
// mark profile as used
localProfile.MarkUsed()
p.LocalProfileKey = localProfile.Key()
p.profile = profile.NewLayeredProfile(localProfile)
go p.Save()
return p
// Save process to storage.
process.Save()
return process, nil
})
return p.(*Process)
}

View File

@@ -7,46 +7,61 @@ import (
)
const (
activeProfileCleanerTickDuration = 10 * time.Minute
activeProfileCleanerThreshold = 1 * time.Hour
activeProfileCleanerTickDuration = 1 * time.Minute
activeProfileCleanerThreshold = 5 * time.Minute
)
var (
// TODO: periodically clean up inactive profiles
activeProfiles = make(map[string]*Profile)
activeProfilesLock sync.RWMutex
)
// getActiveProfile returns a cached copy of an active profile and nil if it isn't found.
func getActiveProfile(scopedID string) *Profile {
activeProfilesLock.Lock()
defer activeProfilesLock.Unlock()
activeProfilesLock.RLock()
defer activeProfilesLock.RUnlock()
profile, ok := activeProfiles[scopedID]
activeProfile, ok := activeProfiles[scopedID]
if ok {
return profile
activeProfile.MarkStillActive()
return activeProfile
}
return nil
}
// markProfileActive registers a profile as active.
func markProfileActive(profile *Profile) {
// findActiveProfile searched for an active local profile using the linked path.
func findActiveProfile(linkedPath string) *Profile {
activeProfilesLock.RLock()
defer activeProfilesLock.RUnlock()
for _, activeProfile := range activeProfiles {
if activeProfile.LinkedPath == linkedPath {
activeProfile.MarkStillActive()
return activeProfile
}
}
return nil
}
// addActiveProfile registers a active profile.
func addActiveProfile(profile *Profile) {
activeProfilesLock.Lock()
defer activeProfilesLock.Unlock()
profile.MarkStillActive()
activeProfiles[profile.ScopedID()] = profile
}
// markActiveProfileAsOutdated marks an active profile as outdated, so that it will be refetched from the database.
// markActiveProfileAsOutdated marks an active profile as outdated.
func markActiveProfileAsOutdated(scopedID string) {
activeProfilesLock.Lock()
defer activeProfilesLock.Unlock()
activeProfilesLock.RLock()
defer activeProfilesLock.RUnlock()
profile, ok := activeProfiles[scopedID]
if ok {
profile.outdated.Set()
delete(activeProfiles, scopedID)
}
}
@@ -55,16 +70,12 @@ func cleanActiveProfiles(ctx context.Context) error {
select {
case <-time.After(activeProfileCleanerTickDuration):
threshold := time.Now().Add(-activeProfileCleanerThreshold)
threshold := time.Now().Add(-activeProfileCleanerThreshold).Unix()
activeProfilesLock.Lock()
for id, profile := range activeProfiles {
// get last used
profile.Lock()
lastUsed := profile.lastUsed
profile.Unlock()
// remove if not used for a while
if lastUsed.Before(threshold) {
// Remove profile if it hasn't been used for a while.
if profile.LastActive() < threshold {
profile.outdated.Set()
delete(activeProfiles, id)
}

View File

@@ -71,13 +71,9 @@ func updateGlobalConfigProfile(ctx context.Context, data interface{}) error {
}
// build global profile for reference
profile := &Profile{
ID: "global-config",
Source: SourceSpecial,
Name: "Global Configuration",
Config: make(map[string]interface{}),
internalSave: true,
}
profile := New(SourceSpecial, "global-config")
profile.Name = "Global Configuration"
profile.Internal = true
newConfig := make(map[string]interface{})
// fill profile config options

View File

@@ -69,10 +69,6 @@ var (
cfgOptionDisableAutoPermit config.IntOption // security level option
cfgOptionDisableAutoPermitOrder = 80
CfgOptionEnforceSPNKey = "filter/enforceSPN"
cfgOptionEnforceSPN config.IntOption // security level option
cfgOptionEnforceSPNOrder = 96
CfgOptionRemoveOutOfScopeDNSKey = "filter/removeOutOfScopeDNS"
cfgOptionRemoveOutOfScopeDNS config.IntOption // security level option
cfgOptionRemoveOutOfScopeDNSOrder = 112
@@ -86,6 +82,10 @@ var (
cfgOptionDomainHeuristicsOrder = 114
// Permanent Verdicts Order = 128
CfgOptionUseSPNKey = "spn/useSPN"
cfgOptionUseSPN config.BoolOption
cfgOptionUseSPNOrder = 128
)
func registerConfiguration() error {
@@ -94,11 +94,11 @@ func registerConfiguration() error {
// ask - ask mode: if not verdict is found, the user is consulted
// block - allowlist mode: everything is blocked unless permitted
err := config.Register(&config.Option{
Name: "Default Filter Action",
Key: CfgOptionDefaultActionKey,
Description: `The default filter action when nothing else permits or blocks a connection.`,
Name: "Default Action",
Key: CfgOptionDefaultActionKey,
// TODO: Discuss "when nothing else"
Description: `The default action when nothing else permits or blocks an outgoing connection. Inbound connections are always blocked by default.`,
OptType: config.OptTypeString,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: "permit",
Annotations: config.Annotations{
config.DisplayHintAnnotation: config.DisplayHintOneOf,
@@ -112,7 +112,7 @@ func registerConfiguration() error {
Description: "Permit all connections",
},
{
Name: "Ask",
Name: "Prompt",
Value: "ask",
Description: "Always ask for a decision",
},
@@ -131,10 +131,12 @@ func registerConfiguration() error {
// Disable Auto Permit
err = config.Register(&config.Option{
// TODO: Discuss
Name: "Disable Auto Permit",
Key: CfgOptionDisableAutoPermitKey,
Description: "Auto Permit searches for a relation between an app and the destionation of a connection - if there is a correlation, the connection will be permitted. This setting is negated in order to provide a streamlined user experience, where higher settings are better.",
Description: `Auto Permit searches for a relation between an app and the destination of a connection - if there is a correlation, the connection will be permitted. This setting is negated in order to provide a streamlined user experience, where "higher settings" provide more protection.`,
OptType: config.OptTypeInt,
ReleaseLevel: config.ReleaseLevelBeta,
DefaultValue: status.SecurityLevelsAll,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionDisableAutoPermitOrder,
@@ -200,7 +202,7 @@ Examples:
err = config.Register(&config.Option{
Name: "Incoming Rules",
Key: CfgOptionServiceEndpointsKey,
Description: "Rules that apply to incoming network connections. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.",
Description: "Rules that apply to incoming network connections. Network Scope restrictions and the incoming permission still apply. Also note that the default action for incoming connections is to always block.",
Help: filterListHelp,
OptType: config.OptTypeStringArray,
DefaultValue: []string{"+ Localhost"},
@@ -236,9 +238,9 @@ Examples:
// Filter list IDs
err = config.Register(&config.Option{
Name: "Filter List",
Name: "Filter Lists",
Key: CfgOptionFilterListsKey,
Description: "Filter connections by matching the endpoint against configured filterlists",
Description: "Block connections that match enabled filter lists.",
OptType: config.OptTypeStringArray,
DefaultValue: []string{"TRAC", "MAL"},
Annotations: config.Annotations{
@@ -256,9 +258,9 @@ Examples:
// Include CNAMEs
err = config.Register(&config.Option{
Name: "Filter CNAMEs",
Name: "Check Domain Aliases",
Key: CfgOptionFilterCNAMEKey,
Description: "Also filter requests where a CNAME would be blocked",
Description: "In addition to checking a domain against rules and filter lists, also check it's resolved CNAMEs.",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelsAll,
ExpertiseLevel: config.ExpertiseLevelExpert,
@@ -277,9 +279,9 @@ Examples:
// Include subdomains
err = config.Register(&config.Option{
Name: "Filter Subdomains",
Name: "Check Subdomains",
Key: CfgOptionFilterSubDomainsKey,
Description: "Also filter a domain if any parent domain is blocked by a filter list",
Description: "Also block a domain if any parent domain is blocked by a filter list",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelsAll,
PossibleValues: status.SecurityLevelValues,
@@ -297,9 +299,9 @@ Examples:
// Block Scope Local
err = config.Register(&config.Option{
Name: "Block Scope Local",
Name: "Block Device-Local Connections",
Key: CfgOptionBlockScopeLocalKey,
Description: "Block internal connections on your own device, ie. localhost.",
Description: "Block all internal connections on your own device, ie. localhost.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
DefaultValue: status.SecurityLevelOff,
@@ -318,9 +320,9 @@ Examples:
// Block Scope LAN
err = config.Register(&config.Option{
Name: "Block Scope LAN",
Name: "Block LAN",
Key: CfgOptionBlockScopeLANKey,
Description: "Block connections to the Local Area Network.",
Description: "Block all connections from and to the Local Area Network.",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelsHighAndExtreme,
PossibleValues: status.AllSecurityLevelValues,
@@ -338,9 +340,9 @@ Examples:
// Block Scope Internet
err = config.Register(&config.Option{
Name: "Block Scope Internet",
Name: "Block Internet",
Key: CfgOptionBlockScopeInternetKey,
Description: "Block connections to the Internet.",
Description: "Block connections from and to the Internet.",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelOff,
PossibleValues: status.AllSecurityLevelValues,
@@ -358,9 +360,9 @@ Examples:
// Block Peer to Peer Connections
err = config.Register(&config.Option{
Name: "Block Peer to Peer Connections",
Name: "Block P2P/Direct Connections",
Key: CfgOptionBlockP2PKey,
Description: "These are connections that are established directly to an IP address on the Internet without resolving a domain name via DNS first.",
Description: "These are connections that are established directly to an IP address or peer on the Internet without resolving a domain name via DNS first.",
OptType: config.OptTypeInt,
DefaultValue: status.SecurityLevelExtreme,
PossibleValues: status.SecurityLevelValues,
@@ -378,7 +380,7 @@ Examples:
// Block Inbound Connections
err = config.Register(&config.Option{
Name: "Block Inbound Connections",
Name: "Block Incoming Connections",
Key: CfgOptionBlockInboundKey,
Description: "Connections initiated towards your device from the LAN or Internet. This will usually only be the case if you are running a network service or are using peer to peer software.",
OptType: config.OptTypeInt,
@@ -396,35 +398,13 @@ Examples:
cfgOptionBlockInbound = config.Concurrent.GetAsInt(CfgOptionBlockInboundKey, int64(status.SecurityLevelsHighAndExtreme))
cfgIntOptions[CfgOptionBlockInboundKey] = cfgOptionBlockInbound
// Enforce SPN
err = config.Register(&config.Option{
Name: "Enforce SPN",
Key: CfgOptionEnforceSPNKey,
Description: "This setting enforces connections to be routed over the SPN. If this is not possible for any reason, connections will be blocked.",
OptType: config.OptTypeInt,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: status.SecurityLevelOff,
PossibleValues: status.AllSecurityLevelValues,
Annotations: config.Annotations{
config.DisplayHintAnnotation: status.DisplayHintSecurityLevel,
config.DisplayOrderAnnotation: cfgOptionEnforceSPNOrder,
config.CategoryAnnotation: "Advanced",
},
})
if err != nil {
return err
}
cfgOptionEnforceSPN = config.Concurrent.GetAsInt(CfgOptionEnforceSPNKey, int64(status.SecurityLevelOff))
cfgIntOptions[CfgOptionEnforceSPNKey] = cfgOptionEnforceSPN
// Filter Out-of-Scope DNS Records
err = config.Register(&config.Option{
Name: "Filter Out-of-Scope DNS Records",
Name: "Enforce global/private split-view",
Key: CfgOptionRemoveOutOfScopeDNSKey,
Description: "Filter DNS answers that are outside of the scope of the server. A server on the public Internet may not respond with a private LAN address.",
Description: "Remove private IP addresses from public DNS responses.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
DefaultValue: status.SecurityLevelsAll,
PossibleValues: status.SecurityLevelValues,
Annotations: config.Annotations{
@@ -441,12 +421,11 @@ Examples:
// Filter DNS Records that would be blocked
err = config.Register(&config.Option{
Name: "Filter DNS Records that would be blocked",
Name: "Remove blocked records",
Key: CfgOptionRemoveBlockedDNSKey,
Description: "Pre-filter DNS answers that an application would not be allowed to connect to.",
Description: "Remove blocked IP addresses from DNS responses.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
DefaultValue: status.SecurityLevelsAll,
PossibleValues: status.SecurityLevelValues,
Annotations: config.Annotations{
@@ -463,9 +442,9 @@ Examples:
// Domain heuristics
err = config.Register(&config.Option{
Name: "Enable Domain Heuristics",
Name: "Domain Heuristics",
Key: CfgOptionDomainHeuristicsKey,
Description: "Domain Heuristics checks for suspicious looking domain names and blocks them. Ths option currently targets domains generated by malware and DNS data tunnels.",
Description: "Domain Heuristics checks for suspicious domain names and blocks them. This option currently targets domain names generated by malware and DNS data exfiltration channels.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
DefaultValue: status.SecurityLevelsAll,
@@ -483,9 +462,10 @@ Examples:
// Bypass prevention
err = config.Register(&config.Option{
Name: "Prevent Bypassing",
Key: CfgOptionPreventBypassingKey,
Description: "Prevent apps from bypassing the privacy filter: Firefox by disabling DNS-over-HTTPs",
Name: "Prevent Bypassing",
Key: CfgOptionPreventBypassingKey,
Description: `Prevent apps from bypassing the privacy filter:
- Disable Firefox' internal DNS-over-HTTPs resolver`,
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelBeta,
@@ -503,5 +483,24 @@ Examples:
cfgOptionPreventBypassing = config.Concurrent.GetAsInt((CfgOptionPreventBypassingKey), int64(status.SecurityLevelsAll))
cfgIntOptions[CfgOptionPreventBypassingKey] = cfgOptionPreventBypassing
// Use SPN
err = config.Register(&config.Option{
Name: "Use SPN",
Key: CfgOptionUseSPNKey,
Description: "Route connection through the Safing Privacy Network. If it is unavailable for any reason, connections will be blocked.",
OptType: config.OptTypeBool,
ReleaseLevel: config.ReleaseLevelExperimental,
DefaultValue: true,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionUseSPNOrder,
config.CategoryAnnotation: "General",
},
})
if err != nil {
return err
}
cfgOptionUseSPN = config.Concurrent.GetAsBool(CfgOptionUseSPNKey, true)
cfgBoolOptions[CfgOptionUseSPNKey] = cfgOptionUseSPN
return nil
}

View File

@@ -1,55 +0,0 @@
package profile
import (
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/log"
)
// FindOrCreateLocalProfileByPath returns an existing or new profile for the given application path.
func FindOrCreateLocalProfileByPath(fullPath string) (profile *Profile, new bool, err error) {
// find local profile
it, err := profileDB.Query(
query.New(makeProfileKey(SourceLocal, "")).Where(
query.Where("LinkedPath", query.SameAs, fullPath),
),
)
if err != nil {
return nil, false, err
}
// get first result
r := <-it.Next
// cancel immediately
it.Cancel()
// return new if none was found
if r == nil {
profile = New()
profile.LinkedPath = fullPath
return profile, true, nil
}
// ensure its a profile
profile, err = EnsureProfile(r)
if err != nil {
return nil, false, err
}
// prepare config
err = profile.prepConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// parse config
err = profile.parseConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// mark active
markProfileActive(profile)
// return parsed profile
return profile, false, nil
}

206
profile/get.go Normal file
View File

@@ -0,0 +1,206 @@
package profile
import (
"errors"
"os"
"strings"
"github.com/safing/portbase/database"
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
"golang.org/x/sync/singleflight"
)
const (
// UnidentifiedProfileID is the profile ID used for unidentified processes.
UnidentifiedProfileID = "_unidentified"
// SystemProfileID is the profile ID used for the system/kernel.
SystemProfileID = "_system"
)
var getProfileSingleInflight singleflight.Group
// GetProfile fetches a profile. This function ensure that the profile loaded
// is shared among all callers. You must always supply both the scopedID and
// linkedPath parameters whenever available.
func GetProfile(source profileSource, id, linkedPath string) ( //nolint:gocognit
profile *Profile,
newProfile bool,
err error,
) {
// Select correct key for single in flight.
singleInflightKey := linkedPath
if singleInflightKey == "" {
singleInflightKey = makeScopedID(source, id)
}
p, err, _ := getProfileSingleInflight.Do(singleInflightKey, func() (interface{}, error) {
var previousVersion *Profile
// Fetch profile depending on the available information.
switch {
case id != "":
scopedID := makeScopedID(source, id)
// Get profile via the scoped ID.
// Check if there already is an active and not outdated profile.
profile = getActiveProfile(scopedID)
if profile != nil {
if profile.outdated.IsSet() {
previousVersion = profile
} else {
return profile, nil
}
}
// Get from database.
profile, err = getProfile(scopedID)
// If we cannot find a profile, check if the request is for a special
// profile we can create.
if errors.Is(err, database.ErrNotFound) {
switch id {
case UnidentifiedProfileID:
profile = New(SourceLocal, UnidentifiedProfileID)
newProfile = true
err = nil
case SystemProfileID:
profile = New(SourceLocal, SystemProfileID)
newProfile = true
err = nil
}
}
case linkedPath != "":
// Search for profile via a linked path.
// Check if there already is an active and not outdated profile for
// the linked path.
profile = findActiveProfile(linkedPath)
if profile != nil {
if profile.outdated.IsSet() {
previousVersion = profile
} else {
return profile, nil
}
}
// Get from database.
profile, newProfile, err = findProfile(linkedPath)
default:
return nil, errors.New("cannot fetch profile without ID or path")
}
if err != nil {
return nil, err
}
// Process profiles coming directly from the database.
// As we don't use any caching, these will be new objects.
// Mark the profile as being saved internally in order to not trigger an
// update after saving it to the database.
profile.internalSave = true
// Add a layeredProfile to local profiles.
if profile.Source == SourceLocal {
// If we are refetching, assign the layered profile from the previous version.
if previousVersion != nil {
profile.layeredProfile = previousVersion.layeredProfile
}
// Local profiles must have a layered profile, create a new one if it
// does not yet exist.
if profile.layeredProfile == nil {
profile.layeredProfile = NewLayeredProfile(profile)
}
}
// Add the profile to the currently active profiles.
addActiveProfile(profile)
return profile, nil
})
if err != nil {
return nil, false, err
}
if p == nil {
return nil, false, errors.New("profile getter returned nil")
}
return p.(*Profile), newProfile, nil
}
// getProfile fetches the profile for the given scoped ID.
func getProfile(scopedID string) (profile *Profile, err error) {
// Get profile from the database.
r, err := profileDB.Get(profilesDBPath + scopedID)
if err != nil {
return nil, err
}
// Parse and prepare the profile, return the result.
return prepProfile(r)
}
// findProfile searches for a profile with the given linked path. If it cannot
// find one, it will create a new profile for the given linked path.
func findProfile(linkedPath string) (profile *Profile, new bool, err error) {
// Search the database for a matching profile.
it, err := profileDB.Query(
query.New(makeProfileKey(SourceLocal, "")).Where(
query.Where("LinkedPath", query.SameAs, linkedPath),
),
)
if err != nil {
return nil, false, err
}
// Only wait for the first result, or until the query ends.
r := <-it.Next
// Then cancel the query, should it still be running.
it.Cancel()
// Prep and return an existing profile.
if r != nil {
profile, err = prepProfile(r)
return profile, false, err
}
// If there was no profile in the database, create a new one, and return it.
profile = New(SourceLocal, "")
profile.LinkedPath = linkedPath
// Check if the profile should be marked as internal.
// This is the case whenever the binary resides within the data root dir.
if strings.HasPrefix(linkedPath, dataroot.Root().Dir+string(os.PathSeparator)) {
profile.Internal = true
}
return profile, true, nil
}
func prepProfile(r record.Record) (*Profile, error) {
// ensure its a profile
profile, err := EnsureProfile(r)
if err != nil {
return nil, err
}
// prepare config
err = profile.prepConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// parse config
err = profile.parseConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// return parsed profile
return profile, nil
}

View File

@@ -38,6 +38,11 @@ func start() error {
return err
}
err = registerRevisionProvider()
if err != nil {
return err
}
err = startProfileUpdateChecker()
if err != nil {
return err

View File

@@ -0,0 +1,49 @@
package profile
import (
"errors"
"strings"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/runtime"
)
const (
revisionProviderPrefix = "runtime:layeredProfile/"
)
var (
errProfileNotActive = errors.New("profile not active")
errNoLayeredProfile = errors.New("profile has no layered profile")
)
func registerRevisionProvider() error {
_, err := runtime.DefaultRegistry.Register(
revisionProviderPrefix,
runtime.SimpleValueGetterFunc(getRevision),
)
return err
}
func getRevision(key string) ([]record.Record, error) {
key = strings.TrimPrefix(key, revisionProviderPrefix)
// Get active profile.
profile := getActiveProfile(key)
if profile == nil {
return nil, errProfileNotActive
}
// Get layered profile.
layeredProfile := profile.LayeredProfile()
if layeredProfile == nil {
return nil, errNoLayeredProfile
}
// Update profiles if necessary.
if layeredProfile.NeedsUpdate() {
layeredProfile.Update()
}
return []record.Record{layeredProfile}, nil
}

View File

@@ -5,48 +5,45 @@ import (
"sync"
"sync/atomic"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/status"
"github.com/tevino/abool"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/profile/endpoints"
)
var (
no = abool.NewBool(false)
)
// LayeredProfile combines multiple Profiles.
type LayeredProfile struct {
lock sync.Mutex
record.Base
sync.RWMutex
localProfile *Profile
layers []*Profile
revisionCounter uint64
localProfile *Profile
layers []*Profile
validityFlag *abool.AtomicBool
validityFlagLock sync.Mutex
LayerIDs []string
RevisionCounter uint64
globalValidityFlag *config.ValidityFlag
securityLevel *uint32
// These functions give layered access to configuration options and require
// the layered profile to be read locked.
DisableAutoPermit config.BoolOption
BlockScopeLocal config.BoolOption
BlockScopeLAN config.BoolOption
BlockScopeInternet config.BoolOption
BlockP2P config.BoolOption
BlockInbound config.BoolOption
EnforceSPN config.BoolOption
RemoveOutOfScopeDNS config.BoolOption
RemoveBlockedDNS config.BoolOption
FilterSubDomains config.BoolOption
FilterCNAMEs config.BoolOption
PreventBypassing config.BoolOption
DomainHeuristics config.BoolOption
UseSPN config.BoolOption
}
// NewLayeredProfile returns a new layered profile based on the given local profile.
@@ -56,8 +53,7 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
new := &LayeredProfile{
localProfile: localProfile,
layers: make([]*Profile, 0, len(localProfile.LinkedProfiles)+1),
revisionCounter: 0,
validityFlag: abool.NewBool(true),
LayerIDs: make([]string, 0, len(localProfile.LinkedProfiles)+1),
globalValidityFlag: config.NewValidityFlag(),
securityLevel: &securityLevelVal,
}
@@ -86,10 +82,6 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
CfgOptionBlockInboundKey,
cfgOptionBlockInbound,
)
new.EnforceSPN = new.wrapSecurityLevelOption(
CfgOptionEnforceSPNKey,
cfgOptionEnforceSPN,
)
new.RemoveOutOfScopeDNS = new.wrapSecurityLevelOption(
CfgOptionRemoveOutOfScopeDNSKey,
cfgOptionRemoveOutOfScopeDNS,
@@ -114,22 +106,44 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
CfgOptionDomainHeuristicsKey,
cfgOptionDomainHeuristics,
)
new.UseSPN = new.wrapBoolOption(
CfgOptionUseSPNKey,
cfgOptionUseSPN,
)
// TODO: load linked profiles.
// FUTURE: load forced company profile
new.LayerIDs = append(new.LayerIDs, localProfile.ScopedID())
new.layers = append(new.layers, localProfile)
// FUTURE: load company profile
// FUTURE: load community profile
// TODO: Load additional profiles.
new.updateCaches()
new.SetKey(revisionProviderPrefix + localProfile.ID)
return new
}
func (lp *LayeredProfile) getValidityFlag() *abool.AtomicBool {
lp.validityFlagLock.Lock()
defer lp.validityFlagLock.Unlock()
return lp.validityFlag
// LockForUsage locks the layered profile, including all layers individually.
func (lp *LayeredProfile) LockForUsage() {
lp.RLock()
for _, layer := range lp.layers {
layer.RLock()
}
}
// UnlockForUsage unlocks the layered profile, including all layers individually.
func (lp *LayeredProfile) UnlockForUsage() {
lp.RUnlock()
for _, layer := range lp.layers {
layer.RUnlock()
}
}
// LocalProfile returns the local profile associated with this layered profile.
func (lp *LayeredProfile) LocalProfile() *Profile {
lp.RLock()
defer lp.RUnlock()
return lp.localProfile
}
// RevisionCnt returns the current profile revision counter.
@@ -138,23 +152,57 @@ func (lp *LayeredProfile) RevisionCnt() (revisionCounter uint64) {
return 0
}
lp.lock.Lock()
defer lp.lock.Unlock()
lp.RLock()
defer lp.RUnlock()
return lp.revisionCounter
return lp.RevisionCounter
}
// Update checks for updated profiles and replaces any outdated profiles.
// MarkStillActive marks all the layers as still active.
func (lp *LayeredProfile) MarkStillActive() {
if lp == nil {
return
}
lp.RLock()
defer lp.RUnlock()
for _, layer := range lp.layers {
layer.MarkStillActive()
}
}
// NeedsUpdate checks for outdated profiles.
func (lp *LayeredProfile) NeedsUpdate() (outdated bool) {
lp.RLock()
defer lp.RUnlock()
// Check global config state.
if !lp.globalValidityFlag.IsValid() {
return true
}
// Check config in layers.
for _, layer := range lp.layers {
if layer.outdated.IsSet() {
return true
}
}
return false
}
// Update checks for and replaces any outdated profiles.
func (lp *LayeredProfile) Update() (revisionCounter uint64) {
lp.lock.Lock()
defer lp.lock.Unlock()
lp.Lock()
defer lp.Unlock()
var changed bool
for i, layer := range lp.layers {
if layer.outdated.IsSet() {
changed = true
// update layer
newLayer, err := GetProfile(layer.Source, layer.ID)
newLayer, _, err := GetProfile(layer.Source, layer.ID, layer.LinkedPath)
if err != nil {
log.Errorf("profiles: failed to update profile %s", layer.ScopedID())
} else {
@@ -167,11 +215,6 @@ func (lp *LayeredProfile) Update() (revisionCounter uint64) {
}
if changed {
// reset validity flag
lp.validityFlagLock.Lock()
lp.validityFlag.SetTo(false)
lp.validityFlag = abool.NewBool(true)
lp.validityFlagLock.Unlock()
// get global config validity flag
lp.globalValidityFlag.Refresh()
@@ -179,10 +222,10 @@ func (lp *LayeredProfile) Update() (revisionCounter uint64) {
lp.updateCaches()
// bump revision counter
lp.revisionCounter++
lp.RevisionCounter++
}
return lp.revisionCounter
return lp.RevisionCounter
}
func (lp *LayeredProfile) updateCaches() {
@@ -194,8 +237,6 @@ func (lp *LayeredProfile) updateCaches() {
}
}
atomic.StoreUint32(lp.securityLevel, uint32(newLevel))
// TODO: ignore community profiles
}
// MarkUsed marks the localProfile as used.
@@ -203,12 +244,12 @@ func (lp *LayeredProfile) MarkUsed() {
lp.localProfile.MarkUsed()
}
// SecurityLevel returns the highest security level of all layered profiles.
// SecurityLevel returns the highest security level of all layered profiles. This function is atomic and does not require any locking.
func (lp *LayeredProfile) SecurityLevel() uint8 {
return uint8(atomic.LoadUint32(lp.securityLevel))
}
// DefaultAction returns the active default action ID.
// DefaultAction returns the active default action ID. This functions requires the layered profile to be read locked.
func (lp *LayeredProfile) DefaultAction() uint8 {
for _, layer := range lp.layers {
if layer.defaultAction > 0 {
@@ -221,7 +262,7 @@ func (lp *LayeredProfile) DefaultAction() uint8 {
return cfgDefaultAction
}
// MatchEndpoint checks if the given endpoint matches an entry in any of the profiles.
// MatchEndpoint checks if the given endpoint matches an entry in any of the profiles. This functions requires the layered profile to be read locked.
func (lp *LayeredProfile) MatchEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
for _, layer := range lp.layers {
if layer.endpoints.IsSet() {
@@ -237,7 +278,7 @@ func (lp *LayeredProfile) MatchEndpoint(ctx context.Context, entity *intel.Entit
return cfgEndpoints.Match(ctx, entity)
}
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles.
// MatchServiceEndpoint checks if the given endpoint of an inbound connection matches an entry in any of the profiles. This functions requires the layered profile to be read locked.
func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
entity.EnableReverseResolving()
@@ -256,7 +297,7 @@ func (lp *LayeredProfile) MatchServiceEndpoint(ctx context.Context, entity *inte
}
// MatchFilterLists matches the entity against the set of filter
// lists.
// lists. This functions requires the layered profile to be read locked.
func (lp *LayeredProfile) MatchFilterLists(ctx context.Context, entity *intel.Entity) (endpoints.EPResult, endpoints.Reason) {
entity.ResolveSubDomainLists(ctx, lp.FilterSubDomains())
entity.EnableCNAMECheck(ctx, lp.FilterCNAMEs())
@@ -287,16 +328,6 @@ func (lp *LayeredProfile) MatchFilterLists(ctx context.Context, entity *intel.En
return endpoints.NoMatch, nil
}
// AddEndpoint adds an endpoint to the local endpoint list, saves the local profile and reloads the configuration.
func (lp *LayeredProfile) AddEndpoint(newEntry string) {
lp.localProfile.AddEndpoint(newEntry)
}
// AddServiceEndpoint adds a service endpoint to the local endpoint list, saves the local profile and reloads the configuration.
func (lp *LayeredProfile) AddServiceEndpoint(newEntry string) {
lp.localProfile.AddServiceEndpoint(newEntry)
}
func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig config.IntOption) config.BoolOption {
activeAtLevels := lp.wrapIntOption(configKey, globalConfig)
@@ -308,22 +339,27 @@ func (lp *LayeredProfile) wrapSecurityLevelOption(configKey string, globalConfig
}
}
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
valid := no
var value int64
func (lp *LayeredProfile) wrapBoolOption(configKey string, globalConfig config.BoolOption) config.BoolOption {
revCnt := lp.RevisionCounter
var value bool
var refreshLock sync.Mutex
return func() int64 {
if !valid.IsSet() {
valid = lp.getValidityFlag()
return func() bool {
refreshLock.Lock()
defer refreshLock.Unlock()
// Check if we need to refresh the value.
if revCnt != lp.RevisionCounter {
revCnt = lp.RevisionCounter
// Go through all layers to find an active value.
found := false
layerLoop:
for _, layer := range lp.layers {
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
layerValue, ok := layer.configPerspective.GetAsBool(configKey)
if ok {
found = true
value = layerValue
break layerLoop
break
}
}
if !found {
@@ -335,25 +371,76 @@ func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.In
}
}
func (lp *LayeredProfile) wrapIntOption(configKey string, globalConfig config.IntOption) config.IntOption {
revCnt := lp.RevisionCounter
var value int64
var refreshLock sync.Mutex
return func() int64 {
refreshLock.Lock()
defer refreshLock.Unlock()
// Check if we need to refresh the value.
if revCnt != lp.RevisionCounter {
revCnt = lp.RevisionCounter
// Go through all layers to find an active value.
found := false
for _, layer := range lp.layers {
layerValue, ok := layer.configPerspective.GetAsInt(configKey)
if ok {
found = true
value = layerValue
break
}
}
if !found {
value = globalConfig()
}
}
return value
}
}
// GetProfileSource returns the database key of the first profile in the
// layers that has the given configuration key set. If it returns an empty
// string, the global profile can be assumed to have been effective.
func (lp *LayeredProfile) GetProfileSource(configKey string) string {
for _, layer := range lp.layers {
if layer.configPerspective.Has(configKey) {
return layer.Key()
}
}
// Global Profile
return ""
}
/*
For later:
func (lp *LayeredProfile) wrapStringOption(configKey string, globalConfig config.StringOption) config.StringOption {
valid := no
revCnt := lp.RevisionCounter
var value string
var refreshLock sync.Mutex
return func() string {
if !valid.IsSet() {
valid = lp.getValidityFlag()
refreshLock.Lock()
defer refreshLock.Unlock()
// Check if we need to refresh the value.
if revCnt != lp.RevisionCounter {
revCnt = lp.RevisionCounter
// Go through all layers to find an active value.
found := false
layerLoop:
for _, layer := range lp.layers {
layerValue, ok := layer.configPerspective.GetAsString(configKey)
if ok {
found = true
value = layerValue
break layerLoop
break
}
}
if !found {

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/tevino/abool"
@@ -53,7 +54,8 @@ const (
// Profile is used to predefine a security profile for applications.
type Profile struct { //nolint:maligned // not worth the effort
record.Base
sync.Mutex
sync.RWMutex
// ID is a unique identifier for the profile.
ID string
// Source describes the source of the profile.
@@ -73,7 +75,6 @@ type Profile struct { //nolint:maligned // not worth the effort
Icon string
// IconType describes the type of the Icon property.
IconType iconType
// References - local profiles only
// LinkedPath is a filesystem path to the executable this
// profile was created for.
LinkedPath string
@@ -88,7 +89,7 @@ type Profile struct { //nolint:maligned // not worth the effort
// Config holds profile specific setttings. It's a nested
// object with keys defining the settings database path. All keys
// until the actual settings value (which is everything that is not
// an object) need to be concatinated for the settings database
// an object) need to be concatenated for the settings database
// path.
Config map[string]interface{}
// ApproxLastUsed holds a UTC timestamp in seconds of
@@ -99,6 +100,17 @@ type Profile struct { //nolint:maligned // not worth the effort
// profile has been created.
Created int64
// Internal is set to true if the profile is attributed to a
// Portmaster internal process. Internal is set during profile
// creation and may be accessed without lock.
Internal bool
// layeredProfile is a link to the layered profile with this profile as the
// main profile.
// All processes with the same binary should share the same instance of the
// local profile and the associated layered profile.
layeredProfile *LayeredProfile
// Interpreted Data
configPerspective *config.Perspective
dataParsed bool
@@ -108,8 +120,8 @@ type Profile struct { //nolint:maligned // not worth the effort
filterListIDs []string
// Lifecycle Management
outdated *abool.AtomicBool
lastUsed time.Time
outdated *abool.AtomicBool
lastActive *int64
internalSave bool
}
@@ -118,6 +130,7 @@ func (profile *Profile) prepConfig() (err error) {
// prepare configuration
profile.configPerspective, err = config.NewPerspective(profile.Config)
profile.outdated = abool.New()
profile.lastActive = new(int64)
return
}
@@ -177,16 +190,24 @@ func (profile *Profile) parseConfig() error {
}
// New returns a new Profile.
func New() *Profile {
func New(source profileSource, id string) *Profile {
profile := &Profile{
ID: utils.RandomUUID("").String(),
Source: SourceLocal,
ID: id,
Source: source,
Created: time.Now().Unix(),
Config: make(map[string]interface{}),
internalSave: true,
}
// create placeholders
// Generate random ID if none is given.
if id == "" {
profile.ID = utils.RandomUUID("").String()
}
// Make key from ID and source.
profile.makeKey()
// Prepare profile to create placeholders.
_ = profile.prepConfig()
_ = profile.parseConfig()
@@ -198,6 +219,11 @@ func (profile *Profile) ScopedID() string {
return makeScopedID(profile.Source, profile.ID)
}
// makeKey derives and sets the record Key from the profile attributes.
func (profile *Profile) makeKey() {
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
}
// Save saves the profile to the database
func (profile *Profile) Save() error {
if profile.ID == "" {
@@ -207,38 +233,41 @@ func (profile *Profile) Save() error {
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
}
if !profile.KeyIsSet() {
profile.SetKey(makeProfileKey(profile.Source, profile.ID))
}
return profileDB.Put(profile)
}
// MarkUsed marks the profile as used and saves it when it has changed.
func (profile *Profile) MarkUsed() {
profile.Lock()
// lastUsed
profile.lastUsed = time.Now()
// MarkStillActive marks the profile as still active.
func (profile *Profile) MarkStillActive() {
atomic.StoreInt64(profile.lastActive, time.Now().Unix())
}
// LastActive returns the unix timestamp when the profile was last marked as
// still active.
func (profile *Profile) LastActive() int64 {
return atomic.LoadInt64(profile.lastActive)
}
// MarkUsed updates ApproxLastUsed when it's been a while and saves the profile if it was changed.
func (profile *Profile) MarkUsed() (changed bool) {
profile.Lock()
defer profile.Unlock()
// ApproxLastUsed
save := false
if time.Now().Add(-lastUsedUpdateThreshold).Unix() > profile.ApproxLastUsed {
profile.ApproxLastUsed = time.Now().Unix()
save = true
return true
}
profile.Unlock()
if save {
err := profile.Save()
if err != nil {
log.Warningf("profiles: failed to save profile %s after marking as used: %s", profile.ScopedID(), err)
}
}
return false
}
// String returns a string representation of the Profile.
func (profile *Profile) String() string {
return profile.Name
return fmt.Sprintf("<%s %s/%s>", profile.Name, profile.Source, profile.ID)
}
// IsOutdated returns whether the this instance of the profile is marked as outdated.
func (profile *Profile) IsOutdated() bool {
return profile.outdated.IsSet()
}
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
@@ -252,82 +281,50 @@ func (profile *Profile) AddServiceEndpoint(newEntry string) {
}
func (profile *Profile) addEndpointyEntry(cfgKey, newEntry string) {
// When finished, save the profile.
defer func() {
err := profile.Save()
if err != nil {
log.Warningf("profile: failed to save profile %s after add an endpoint rule: %s", profile.ScopedID(), err)
}
}()
// When finished increase the revision counter of the layered profile.
defer func() {
if profile.layeredProfile != nil {
profile.layeredProfile.Lock()
defer profile.layeredProfile.Unlock()
profile.layeredProfile.RevisionCounter++
}
}()
// Lock the profile for editing.
profile.Lock()
// get, update, save endpoints list
defer profile.Unlock()
// Get the endpoint list configuration value and add the new entry.
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
if !ok {
endpointList = make([]string, 0, 1)
}
endpointList = append(endpointList, newEntry)
endpointList = append([]string{newEntry}, endpointList...)
config.PutValueIntoHierarchicalConfig(profile.Config, cfgKey, endpointList)
profile.Unlock()
err := profile.Save()
if err != nil {
log.Warningf("profile: failed to save profile after adding endpoint: %s", err)
}
// reload manually
profile.Lock()
// Reload the profile manually in order to parse the newly added entry.
profile.dataParsed = false
err = profile.parseConfig()
err := profile.parseConfig()
if err != nil {
log.Warningf("profile: failed to parse profile config after adding endpoint: %s", err)
log.Warningf("profile: failed to parse %s config after adding endpoint: %s", profile, err)
}
profile.Unlock()
}
// GetProfile loads a profile from the database.
func GetProfile(source profileSource, id string) (*Profile, error) {
return GetProfileByScopedID(makeScopedID(source, id))
}
// GetProfileByScopedID loads a profile from the database using a scoped ID like "local/id" or "community/id".
func GetProfileByScopedID(scopedID string) (*Profile, error) {
// check cache
profile := getActiveProfile(scopedID)
if profile != nil {
profile.MarkUsed()
return profile, nil
}
// get from database
r, err := profileDB.Get(profilesDBPath + scopedID)
if err != nil {
return nil, err
}
// convert
profile, err = EnsureProfile(r)
if err != nil {
return nil, err
}
// lock for prepping
// LayeredProfile returns the layered profile associated with this profile.
func (profile *Profile) LayeredProfile() *LayeredProfile {
profile.Lock()
defer profile.Unlock()
// prepare config
err = profile.prepConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// parse config
err = profile.parseConfig()
if err != nil {
log.Warningf("profiles: profile %s has (partly) invalid configuration: %s", profile.ID, err)
}
// mark as internal
profile.internalSave = true
profile.Unlock()
// mark active
profile.MarkUsed()
markProfileActive(profile)
return profile, nil
return profile.layeredProfile
}
// EnsureProfile ensures that the given record is a *Profile, and returns it.

View File

@@ -1,56 +0,0 @@
package profile
import (
"github.com/safing/portbase/log"
)
const (
unidentifiedProfileID = "_unidentified"
systemProfileID = "_system"
)
// GetUnidentifiedProfile returns the special profile assigned to unidentified processes.
func GetUnidentifiedProfile() *Profile {
// get profile
profile, err := GetProfile(SourceLocal, unidentifiedProfileID)
if err == nil {
return profile
}
// create if not available (or error)
profile = New()
profile.Name = "Unidentified Processes"
profile.Source = SourceLocal
profile.ID = unidentifiedProfileID
// save to db
err = profile.Save()
if err != nil {
log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err)
}
return profile
}
// GetSystemProfile returns the special profile used for the Kernel.
func GetSystemProfile() *Profile {
// get profile
profile, err := GetProfile(SourceLocal, systemProfileID)
if err == nil {
return profile
}
// create if not available (or error)
profile = New()
profile.Name = "Operating System"
profile.Source = SourceLocal
profile.ID = systemProfileID
// save to db
err = profile.Save()
if err != nil {
log.Warningf("profiles: failed to save %s: %s", profile.ScopedID(), err)
}
return profile
}

View File

@@ -82,30 +82,23 @@ func prepConfig() error {
Name: "DNS Servers",
Key: CfgOptionNameServersKey,
Description: "DNS Servers to use for resolving DNS requests.",
Help: `Format:
Help: strings.ReplaceAll(`DNS Servers are configured in a URL format. This allows you to specify special settings for a resolver. If you just want to use a resolver at IP 10.2.3.4, please enter: "dns://10.2.3.4"
The format is: "protocol://ip:port?parameter=value&parameter=value"
DNS Servers are configured in a URL format. This allows you to specify special settings for a resolver. If you just want to use a resolver at IP 10.2.3.4, please enter: dns://10.2.3.4:53
The format is: protocol://ip:port?parameter=value&parameter=value
Protocols:
dot: DNS-over-TLS (recommended)
dns: plain old DNS
tcp: plain old DNS over TCP
IP:
always use the IP address and _not_ the domain name!
Port:
optionally define a custom port
Parameters:
name: give your DNS Server a name that is used for messages and logs
verify: domain name to verify for "dot", required and only valid for "dot"
blockedif: detect if the name server blocks a query, options:
empty: server replies with NXDomain status, but without any other record in any section
refused: server replies with Refused status
zeroip: server replies with an IP address, but it is zero
`,
- Protocol
- "dot": DNS-over-TLS (recommended)
- "dns": plain old DNS
- "tcp": plain old DNS over TCP
- IP: always use the IP address and _not_ the domain name!
- Port: optionally define a custom port
- Parameters:
- "name": give your DNS Server a name that is used for messages and logs
- "verify": domain name to verify for "dot", required and only valid for protocol "dot"
- "blockedif": detect if the name server blocks a query, options:
- "empty": server replies with NXDomain status, but without any other record in any section
- "refused": server replies with Refused status
- "zeroip": server replies with an IP address, but it is zero
`, `"`, "`"),
OptType: config.OptTypeStringArray,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
@@ -157,13 +150,13 @@ Parameters:
configuredNameServers = config.Concurrent.GetAsStringArray(CfgOptionNameServersKey, defaultNameServers)
err = config.Register(&config.Option{
Name: "DNS Server Retry Rate",
Name: "Retry Timeout",
Key: CfgOptionNameserverRetryRateKey,
Description: "Rate at which to retry failed DNS Servers, in seconds.",
Description: "Timeout between retries when a resolver fails.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
DefaultValue: 600,
DefaultValue: 300,
Annotations: config.Annotations{
config.DisplayOrderAnnotation: cfgOptionNameserverRetryRateOrder,
config.UnitAnnotation: "seconds",
@@ -176,9 +169,9 @@ Parameters:
nameserverRetryRate = config.Concurrent.GetAsInt(CfgOptionNameserverRetryRateKey, 600)
err = config.Register(&config.Option{
Name: "Do not use assigned Nameservers",
Name: "Ignore system resolvers",
Key: CfgOptionNoAssignedNameserversKey,
Description: "that were acquired by the network (dhcp) or system",
Description: "Ignore resolvers that were acquired from the operating system.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
@@ -196,9 +189,9 @@ Parameters:
noAssignedNameservers = status.SecurityLevelOption(CfgOptionNoAssignedNameserversKey)
err = config.Register(&config.Option{
Name: "Do not use Multicast DNS",
Name: "Ignore Multicast DNS",
Key: CfgOptionNoMulticastDNSKey,
Description: "Multicast DNS queries other devices in the local network",
Description: "Do not resolve using Multicast DNS. This may break certain Plug and Play devices or services.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
@@ -216,9 +209,9 @@ Parameters:
noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey)
err = config.Register(&config.Option{
Name: "Do not resolve insecurely",
Name: "Enforce secure DNS",
Key: CfgOptionNoInsecureProtocolsKey,
Description: "Do not resolve domains with insecure protocols, ie. plain DNS",
Description: "Never resolve using insecure protocols, ie. plain DNS.",
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,
@@ -236,9 +229,9 @@ Parameters:
noInsecureProtocols = status.SecurityLevelOption(CfgOptionNoInsecureProtocolsKey)
err = config.Register(&config.Option{
Name: "Do not resolve special domains",
Name: "Block unofficial TLDs",
Key: CfgOptionDontResolveSpecialDomainsKey,
Description: fmt.Sprintf("Do not resolve the special top level domains %s", formatScopeList(specialServiceDomains)),
Description: fmt.Sprintf("Block %s.", formatScopeList(specialServiceDomains)),
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,

View File

@@ -45,15 +45,17 @@ type ThreatPayload struct {
// // Once you're done, delete the threat
// threat.Delete().Publish()
//
func NewThreat(id, msg string) *Threat {
func NewThreat(id, title, msg string) *Threat {
t := &Threat{
Notification: &notifications.Notification{
EventID: id,
Message: msg,
Type: notifications.Warning,
State: notifications.Active,
EventID: id,
Type: notifications.Warning,
Title: title,
Category: "Threat",
Message: msg,
},
}
t.threatData().Started = time.Now().Unix()
return t

View File

@@ -25,10 +25,10 @@ func registerConfig() error {
err := config.Register(&config.Option{
Name: "Release Channel",
Key: releaseChannelKey,
Description: "The Release Channel changes which updates are applied. When using beta, you will receive new features earlier and Portmaster will update more frequently. Some beta or experimental features are also available in the stable release channel.",
Description: "Switch release channel.",
OptType: config.OptTypeString,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelBeta,
ExpertiseLevel: config.ExpertiseLevelDeveloper,
ReleaseLevel: config.ReleaseLevelExperimental,
RequiresRestart: false,
DefaultValue: releaseChannelStable,
PossibleValues: []config.PossibleValue{
@@ -54,7 +54,7 @@ func registerConfig() error {
err = config.Register(&config.Option{
Name: "Disable Updates",
Key: disableUpdatesKey,
Description: "Disable automatic updates.",
Description: "Disable automatic updates. This affects all kinds of updates, including intelligence feeds and broadcast notifications.",
OptType: config.OptTypeBool,
ExpertiseLevel: config.ExpertiseLevelExpert,
ReleaseLevel: config.ReleaseLevelStable,

View File

@@ -99,18 +99,30 @@ func upgradeCoreNotify() error {
// check for new version
if info.GetInfo().Version != pmCoreUpdate.Version() {
n := notifications.NotifyInfo(
"updates:core-update-available",
fmt.Sprintf(":tada: Update to **Portmaster v%s** is available! Please restart the Portmaster to apply the update.", pmCoreUpdate.Version()),
notifications.Action{
ID: "restart",
Text: "Restart",
n := notifications.Notify(&notifications.Notification{
EventID: "updates:core-update-available",
Type: notifications.Info,
Title: fmt.Sprintf(
"Portmaster Update v%s",
pmCoreUpdate.Version(),
),
Category: "Core",
Message: fmt.Sprintf(
`:tada: Update to **Portmaster v%s** is available!
Please restart the Portmaster to apply the update.`,
pmCoreUpdate.Version(),
),
AvailableActions: []*notifications.Action{
{
ID: "restart",
Text: "Restart",
},
{
ID: "later",
Text: "Not now",
},
},
notifications.Action{
ID: "later",
Text: "Not now",
},
)
})
n.SetActionFunction(upgradeCoreNotifyActionHandler)
log.Debugf("updates: new portmaster version available, sending notification to user")
@@ -119,7 +131,7 @@ func upgradeCoreNotify() error {
return nil
}
func upgradeCoreNotifyActionHandler(n *notifications.Notification) {
func upgradeCoreNotifyActionHandler(_ context.Context, n *notifications.Notification) error {
switch n.SelectedActionID {
case "restart":
// Cannot directly trigger due to import loop.
@@ -130,11 +142,13 @@ func upgradeCoreNotifyActionHandler(n *notifications.Notification) {
nil,
)
if err != nil {
log.Warningf("updates: failed to trigger restart via notification: %s", err)
return fmt.Errorf("failed to trigger restart via notification: %s", err)
}
case "later":
n.Expires = time.Now().Unix() // expire immediately
return n.Delete()
}
return nil
}
func upgradeHub() error {
@@ -244,10 +258,18 @@ func warnOnIncorrectParentPath() {
if !strings.HasPrefix(absPath, root) {
log.Warningf("detected unexpected path %s for portmaster-start", absPath)
notifications.NotifyWarn(
"updates:unsupported-parent",
fmt.Sprintf("The portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.", expectedFileName, absPath, filepath.Join(root, expectedFileName)),
)
notifications.Notify(&notifications.Notification{
EventID: "updates:unsupported-parent",
Type: notifications.Warning,
Title: "Unsupported Launcher",
Category: "Core",
Message: fmt.Sprintf(
"The portmaster has been launched by an unexpected %s binary at %s. Please configure your system to use the binary at %s as this version will be kept up to date automatically.",
expectedFileName,
absPath,
filepath.Join(root, expectedFileName),
),
})
}
}