Fix and improve prompting
This commit is contained in:
@@ -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: 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.
|
||||
// build notification ID
|
||||
@@ -37,134 +79,142 @@ 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
|
||||
}
|
||||
|
||||
n = ¬ifications.Notification{
|
||||
EventID: nID,
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func registerConfiguration() error {
|
||||
Description: "Permit all connections",
|
||||
},
|
||||
{
|
||||
Name: "Ask",
|
||||
Name: "Prompt",
|
||||
Value: "ask",
|
||||
Description: "Always ask for a decision",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user