mirror of
https://github.com/safing/portmaster
synced 2025-04-20 02:49:10 +00:00
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
327 lines
8.8 KiB
Go
327 lines
8.8 KiB
Go
package firewall
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/safing/portmaster/base/log"
|
|
"github.com/safing/portmaster/base/notifications"
|
|
"github.com/safing/portmaster/service/intel"
|
|
"github.com/safing/portmaster/service/network"
|
|
"github.com/safing/portmaster/service/profile"
|
|
"github.com/safing/portmaster/service/profile/endpoints"
|
|
)
|
|
|
|
const (
|
|
// notification action IDs.
|
|
allowDomainAll = "allow-domain-all"
|
|
allowDomainDistinct = "allow-domain-distinct"
|
|
blockDomainAll = "block-domain-all"
|
|
blockDomainDistinct = "block-domain-distinct"
|
|
|
|
allowIP = "allow-ip"
|
|
blockIP = "block-ip"
|
|
allowServingIP = "allow-serving-ip"
|
|
blockServingIP = "block-serving-ip"
|
|
|
|
cancelPrompt = "cancel"
|
|
)
|
|
|
|
var (
|
|
promptNotificationCreation sync.Mutex
|
|
|
|
decisionTimeout int64 = 10 // in seconds
|
|
)
|
|
|
|
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) {
|
|
// Create notification.
|
|
n := createPrompt(ctx, conn)
|
|
if n == nil {
|
|
// createPrompt returns nil when no further action should be taken.
|
|
return
|
|
}
|
|
|
|
// Add prompt to connection.
|
|
conn.SetPrompt(n)
|
|
|
|
// Get decision timeout and make sure it does not exceed the ask timeout.
|
|
timeout := decisionTimeout
|
|
if timeout > askTimeout() {
|
|
timeout = askTimeout()
|
|
}
|
|
|
|
// wait for response/timeout
|
|
select {
|
|
case promptResponse := <-n.Response():
|
|
switch promptResponse {
|
|
case allowDomainAll, allowDomainDistinct, allowIP, allowServingIP:
|
|
// Accept
|
|
conn.Accept("allowed via prompt", profile.CfgOptionEndpointsKey)
|
|
case "":
|
|
// Dismissed
|
|
conn.Deny("prompting canceled, waiting for new decision", profile.CfgOptionDefaultActionKey)
|
|
default:
|
|
// Deny
|
|
conn.Deny("blocked via prompt", profile.CfgOptionEndpointsKey)
|
|
}
|
|
|
|
case <-time.After(time.Duration(timeout) * time.Second):
|
|
log.Tracer(ctx).Debugf("filter: continuing prompting async")
|
|
conn.Deny("prompting in progress, please respond to prompt", profile.CfgOptionDefaultActionKey)
|
|
|
|
case <-ctx.Done():
|
|
log.Tracer(ctx).Debugf("filter: aborting prompting because of shutdown")
|
|
conn.Drop("shutting down", noReasonOptionKey)
|
|
}
|
|
}
|
|
|
|
// promptIDPrefix is an identifier for privacy filter prompts. This is also used
|
|
// in the UI, so don't change!
|
|
const promptIDPrefix = "filter:prompt"
|
|
|
|
func createPrompt(ctx context.Context, conn *network.Connection) (n *notifications.Notification) {
|
|
expires := time.Now().Add(time.Duration(askTimeout()) * time.Second).Unix()
|
|
|
|
// Get local profile.
|
|
layeredProfile := conn.Process().Profile()
|
|
if layeredProfile == nil {
|
|
log.Tracer(ctx).Warningf("filter: tried creating prompt for connection without profile")
|
|
return nil
|
|
}
|
|
localProfile := layeredProfile.LocalProfile()
|
|
if localProfile == nil {
|
|
log.Tracer(ctx).Warningf("filter: tried creating prompt for connection without local profile")
|
|
return nil
|
|
}
|
|
|
|
// first check if there is an existing notification for this.
|
|
// build notification ID
|
|
var nID string
|
|
switch {
|
|
case conn.Inbound, conn.Entity.Domain == "": // connection to/from IP
|
|
nID = fmt.Sprintf(
|
|
"%s-%s-%v-%s",
|
|
promptIDPrefix,
|
|
localProfile.ID,
|
|
conn.Inbound,
|
|
conn.Entity.IP,
|
|
)
|
|
default: // connection to domain
|
|
nID = fmt.Sprintf(
|
|
"%s-%s-%s",
|
|
promptIDPrefix,
|
|
localProfile.ID,
|
|
conn.Entity.Domain,
|
|
)
|
|
}
|
|
|
|
// 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 {
|
|
// Get notification state and action.
|
|
n.Lock()
|
|
state := n.State
|
|
action := n.SelectedActionID
|
|
n.Unlock()
|
|
|
|
// If the notification is still active, extend and return.
|
|
// This can happen because user input (prompts changing the endpoint
|
|
// lists) can happen any time - also between checking the endpoint lists
|
|
// and now.
|
|
if state == notifications.Active {
|
|
n.Update(expires)
|
|
log.Tracer(ctx).Debugf("filter: updated existing prompt notification")
|
|
return n
|
|
}
|
|
|
|
// The notification is not active anymore, let's check if there is an
|
|
// action we can perform.
|
|
// If there already is an action defined, we won't be fast enough to
|
|
// receive the action with n.Response(), so we take direct action here.
|
|
if action != "" {
|
|
switch action {
|
|
case allowDomainAll, allowDomainDistinct, allowIP, allowServingIP:
|
|
conn.Accept("allowed via prompt", profile.CfgOptionEndpointsKey)
|
|
default: // deny
|
|
conn.Deny("blocked via prompt", profile.CfgOptionEndpointsKey)
|
|
}
|
|
return nil // Do not take further action.
|
|
}
|
|
|
|
// Continue to create a new notification because the previous one is not
|
|
// active and not actionable.
|
|
}
|
|
|
|
// Reference relevant data for save function
|
|
entity := conn.Entity
|
|
// Also needed: localProfile
|
|
|
|
// Create new notification.
|
|
n = ¬ifications.Notification{
|
|
EventID: nID,
|
|
Type: notifications.Prompt,
|
|
Title: "Connection Prompt",
|
|
Category: "Privacy Filter",
|
|
ShowOnSystem: askWithSystemNotifications(),
|
|
EventData: &promptData{
|
|
Entity: entity,
|
|
Profile: promptProfile{
|
|
Source: string(localProfile.Source),
|
|
ID: localProfile.ID,
|
|
// LinkedPath is used to enhance the display of the prompt in the UI.
|
|
// TODO: Using the process path is a workaround. Find a cleaner solution.
|
|
LinkedPath: conn.Process().Path,
|
|
},
|
|
},
|
|
Expires: expires,
|
|
}
|
|
|
|
// Set action function.
|
|
n.SetActionFunction(func(_ context.Context, n *notifications.Notification) error {
|
|
return saveResponse(
|
|
localProfile,
|
|
entity,
|
|
n.SelectedActionID,
|
|
)
|
|
})
|
|
|
|
// Get name of profile for notification. The profile is read-locked by the firewall handler.
|
|
profileName := localProfile.Name
|
|
|
|
// add message and actions
|
|
switch {
|
|
case conn.Inbound:
|
|
n.Message = fmt.Sprintf("%s wants to accept connections from %s (%d/%d)", profileName, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
|
n.AvailableActions = []*notifications.Action{
|
|
{
|
|
ID: allowServingIP,
|
|
Text: "Allow",
|
|
},
|
|
{
|
|
ID: blockServingIP,
|
|
Text: "Block",
|
|
},
|
|
}
|
|
case conn.Entity.Domain == "": // direct connection
|
|
n.Message = fmt.Sprintf("%s wants to connect to %s (%d/%d)", profileName, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port)
|
|
n.AvailableActions = []*notifications.Action{
|
|
{
|
|
ID: allowIP,
|
|
Text: "Allow",
|
|
},
|
|
{
|
|
ID: blockIP,
|
|
Text: "Block",
|
|
},
|
|
}
|
|
default: // connection to domain
|
|
n.Message = fmt.Sprintf("%s wants to connect to %s", profileName, conn.Entity.Domain)
|
|
n.AvailableActions = []*notifications.Action{
|
|
{
|
|
ID: allowDomainAll,
|
|
Text: "Allow",
|
|
},
|
|
{
|
|
ID: blockDomainAll,
|
|
Text: "Block",
|
|
},
|
|
}
|
|
}
|
|
|
|
n.Save()
|
|
log.Tracer(ctx).Debugf("filter: sent prompt notification")
|
|
|
|
return n
|
|
}
|
|
|
|
// promptSavingLock makes sure that only one prompt is saved at a time.
|
|
// Should prompts be persisted in bulk, the next save process might load an
|
|
// outdated profile and save it, losing config data.
|
|
var promptSavingLock sync.Mutex
|
|
|
|
func saveResponse(p *profile.Profile, entity *intel.Entity, promptResponse string) error {
|
|
if promptResponse == cancelPrompt {
|
|
return nil
|
|
}
|
|
|
|
promptSavingLock.Lock()
|
|
defer promptSavingLock.Unlock()
|
|
|
|
// Update the profile if necessary.
|
|
if p.IsOutdated() {
|
|
var err error
|
|
p, err = profile.GetLocalProfile(p.ID, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var ep endpoints.Endpoint
|
|
switch promptResponse {
|
|
case allowDomainAll:
|
|
ep = &endpoints.EndpointDomain{
|
|
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
|
OriginalValue: "." + entity.Domain,
|
|
}
|
|
case allowDomainDistinct:
|
|
ep = &endpoints.EndpointDomain{
|
|
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
|
OriginalValue: entity.Domain,
|
|
}
|
|
case blockDomainAll:
|
|
ep = &endpoints.EndpointDomain{
|
|
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
|
OriginalValue: "." + entity.Domain,
|
|
}
|
|
case blockDomainDistinct:
|
|
ep = &endpoints.EndpointDomain{
|
|
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
|
OriginalValue: entity.Domain,
|
|
}
|
|
case allowIP, allowServingIP:
|
|
ep = &endpoints.EndpointIP{
|
|
EndpointBase: endpoints.EndpointBase{Permitted: true},
|
|
IP: entity.IP,
|
|
}
|
|
case blockIP, blockServingIP:
|
|
ep = &endpoints.EndpointIP{
|
|
EndpointBase: endpoints.EndpointBase{Permitted: false},
|
|
IP: entity.IP,
|
|
}
|
|
case cancelPrompt:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown prompt response: %s", promptResponse)
|
|
}
|
|
|
|
switch promptResponse {
|
|
case allowServingIP, blockServingIP:
|
|
p.AddServiceEndpoint(ep.String())
|
|
log.Infof("filter: added incoming rule to profile %s (LP Rev. %d): %q",
|
|
p, p.LayeredProfile().RevisionCnt(), ep.String())
|
|
default:
|
|
p.AddEndpoint(ep.String())
|
|
log.Infof("filter: added outgoing rule to profile %s (LP Rev. %d): %q",
|
|
p, p.LayeredProfile().RevisionCnt(), ep.String())
|
|
}
|
|
|
|
return nil
|
|
}
|