Fix and improve prompting

This commit is contained in:
Daniel
2020-10-29 16:26:40 +01:00
parent 18a1386bc5
commit b7f0b851ae
2 changed files with 177 additions and 127 deletions

View File

@@ -1,15 +1,18 @@
package firewall package firewall
import ( import (
"context"
"fmt" "fmt"
"sync"
"time" "time"
"github.com/safing/portmaster/profile/endpoints"
"github.com/safing/portbase/log" "github.com/safing/portbase/log"
"github.com/safing/portbase/notifications" "github.com/safing/portbase/notifications"
"github.com/safing/portmaster/intel"
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/profile"
"github.com/safing/portmaster/profile/endpoints"
) )
const ( const (
@@ -25,8 +28,47 @@ const (
denyServingIP = "deny-serving-ip" denyServingIP = "deny-serving-ip"
) )
func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // TODO var (
nTTL := time.Duration(askTimeout()) * time.Second 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: continueing 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. // first check if there is an existing notification for this.
// build notification ID // build notification ID
@@ -37,134 +79,142 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit //
default: // connection to domain default: // connection to domain
nID = fmt.Sprintf("filter:prompt-%d-%s", conn.Process().Pid, conn.Scope) 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 { if n != nil {
// update with new expiry n.Update(expires)
n.Update(time.Now().Add(nTTL).Unix()) log.Tracer(ctx).Debugf("filter: updated existing prompt notification")
// do not save response to profile return
saveResponse = false }
} else {
var ( n = &notifications.Notification{
msg string EventID: nID,
actions []notifications.Action Type: notifications.Prompt,
EventData: conn.Entity,
Expires: expires,
}
// Set action function.
localProfile := conn.Process().Profile().LocalProfile()
entity := conn.Entity
n.SetActionFunction(func(_ context.Context, n *notifications.Notification) error {
return saveResponse(
localProfile,
entity,
n.SelectedActionID,
) )
})
// add message and actions // add message and actions
switch { switch {
case conn.Inbound: 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) 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)
actions = []notifications.Action{ n.AvailableActions = []*notifications.Action{
{ {
ID: permitServingIP, ID: permitServingIP,
Text: "Permit", Text: "Permit",
}, },
{ {
ID: denyServingIP, ID: denyServingIP,
Text: "Deny", Text: "Deny",
}, },
} }
case conn.Entity.Domain == "": // direct connection 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) 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)
actions = []notifications.Action{ n.AvailableActions = []*notifications.Action{
{ {
ID: permitIP, ID: permitIP,
Text: "Permit", Text: "Permit",
}, },
{ {
ID: denyIP, ID: denyIP,
Text: "Deny", Text: "Deny",
}, },
} }
default: // connection to domain default: // connection to domain
if pkt != nil { n.Message = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain)
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) n.AvailableActions = []*notifications.Action{
} else { {
msg = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain) ID: permitDomainAll,
} Text: "Permit all",
actions = []notifications.Action{ },
{ {
ID: permitDomainAll, ID: permitDomainDistinct,
Text: "Permit all", Text: "Permit",
}, },
{ {
ID: permitDomainDistinct, ID: denyDomainDistinct,
Text: "Permit", Text: "Deny",
}, },
{
ID: denyDomainDistinct,
Text: "Deny",
},
}
} }
n = notifications.NotifyPrompt(nID, msg, actions...)
} }
// wait for response/timeout n.Save()
select { log.Tracer(ctx).Debugf("filter: sent prompt notification")
case promptResponse := <-n.Response():
switch promptResponse {
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
conn.Accept("permitted by user")
default: // deny
conn.Deny("denied by user")
}
// end here if we won't save the response to the profile return n
if !saveResponse { }
return
} func saveResponse(p *profile.Profile, entity *intel.Entity, promptResponse string) error {
// Update the profile if necessary.
// get profile if p.IsOutdated() {
p := conn.Process().Profile() var err error
p, _, err = profile.GetProfile(p.Source, p.ID, p.LinkedPath)
var ep endpoints.Endpoint if err != nil {
switch promptResponse { return err
case permitDomainAll: }
ep = &endpoints.EndpointDomain{ }
EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: "." + conn.Entity.Domain, var ep endpoints.Endpoint
} switch promptResponse {
case permitDomainDistinct: case permitDomainAll:
ep = &endpoints.EndpointDomain{ ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true}, EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: conn.Entity.Domain, OriginalValue: "." + entity.Domain,
} }
case denyDomainAll: case permitDomainDistinct:
ep = &endpoints.EndpointDomain{ ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false}, EndpointBase: endpoints.EndpointBase{Permitted: true},
Domain: "." + conn.Entity.Domain, OriginalValue: entity.Domain,
} }
case denyDomainDistinct: case denyDomainAll:
ep = &endpoints.EndpointDomain{ ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: false}, EndpointBase: endpoints.EndpointBase{Permitted: false},
Domain: conn.Entity.Domain, OriginalValue: "." + entity.Domain,
} }
case permitIP, permitServingIP: case denyDomainDistinct:
ep = &endpoints.EndpointIP{ ep = &endpoints.EndpointDomain{
EndpointBase: endpoints.EndpointBase{Permitted: true}, EndpointBase: endpoints.EndpointBase{Permitted: false},
IP: conn.Entity.IP, OriginalValue: entity.Domain,
} }
case denyIP, denyServingIP: case permitIP, permitServingIP:
ep = &endpoints.EndpointIP{ ep = &endpoints.EndpointIP{
EndpointBase: endpoints.EndpointBase{Permitted: false}, EndpointBase: endpoints.EndpointBase{Permitted: true},
IP: conn.Entity.IP, IP: entity.IP,
} }
default: case denyIP, denyServingIP:
log.Warningf("filter: unknown prompt response: %s", promptResponse) ep = &endpoints.EndpointIP{
return EndpointBase: endpoints.EndpointBase{Permitted: false},
} IP: entity.IP,
}
switch promptResponse { default:
case permitServingIP, denyServingIP: return fmt.Errorf("unknown prompt response: %s", promptResponse)
p.AddServiceEndpoint(ep.String()) }
default:
p.AddEndpoint(ep.String()) switch promptResponse {
} case permitServingIP, denyServingIP:
p.AddServiceEndpoint(ep.String())
case <-n.Expired(): log.Infof("filter: added incoming rule to profile %s: %q", p, ep.String())
conn.Deny("no response to prompt") default:
} p.AddEndpoint(ep.String())
log.Infof("filter: added outgoing rule to profile %s: %q", p, ep.String())
}
return nil
} }

View File

@@ -112,7 +112,7 @@ func registerConfiguration() error {
Description: "Permit all connections", Description: "Permit all connections",
}, },
{ {
Name: "Ask", Name: "Prompt",
Value: "ask", Value: "ask",
Description: "Always ask for a decision", Description: "Always ask for a decision",
}, },