mirror of
https://github.com/safing/portmaster
synced 2025-09-02 10:39:22 +00:00
Revamp/cleanup firewall prompting
This commit is contained in:
parent
4b2ff39246
commit
fc8fab1a03
3 changed files with 214 additions and 219 deletions
|
@ -9,6 +9,8 @@ var (
|
||||||
permanentVerdicts config.BoolOption
|
permanentVerdicts config.BoolOption
|
||||||
filterDNSByScope status.SecurityLevelOption
|
filterDNSByScope status.SecurityLevelOption
|
||||||
filterDNSByProfile status.SecurityLevelOption
|
filterDNSByProfile status.SecurityLevelOption
|
||||||
|
promptTimeout config.IntOption
|
||||||
|
|
||||||
devMode config.BoolOption
|
devMode config.BoolOption
|
||||||
apiListenAddress config.StringOption
|
apiListenAddress config.StringOption
|
||||||
)
|
)
|
||||||
|
@ -57,6 +59,19 @@ func registerConfig() error {
|
||||||
}
|
}
|
||||||
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
|
filterDNSByProfile = status.ConfigIsActiveConcurrent("firewall/filterDNSByProfile")
|
||||||
|
|
||||||
|
err = config.Register(&config.Option{
|
||||||
|
Name: "Timeout for prompt notifications",
|
||||||
|
Key: "firewall/promptTimeout",
|
||||||
|
Description: "Amount of time how long Portmaster will wait for a response when prompting about a connection via a notification. In seconds.",
|
||||||
|
ExpertiseLevel: config.ExpertiseLevelUser,
|
||||||
|
OptType: config.OptTypeInt,
|
||||||
|
DefaultValue: 60,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
promptTimeout = config.Concurrent.GetAsInt("firewall/promptTimeout", 30)
|
||||||
|
|
||||||
devMode = config.Concurrent.GetAsBool("firewall/permanentVerdicts", false)
|
devMode = config.Concurrent.GetAsBool("firewall/permanentVerdicts", false)
|
||||||
apiListenAddress = config.GetAsString("api/listenAddress", "")
|
apiListenAddress = config.GetAsString("api/listenAddress", "")
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,9 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portbase/notifications"
|
|
||||||
"github.com/safing/portmaster/intel"
|
"github.com/safing/portmaster/intel"
|
||||||
"github.com/safing/portmaster/network"
|
"github.com/safing/portmaster/network"
|
||||||
"github.com/safing/portmaster/network/netutils"
|
"github.com/safing/portmaster/network/netutils"
|
||||||
|
@ -137,93 +135,7 @@ func DecideOnCommunicationAfterIntel(comm *network.Communication, fqdn string, r
|
||||||
}
|
}
|
||||||
|
|
||||||
// prompt
|
// prompt
|
||||||
|
prompt(comm, nil, nil, fqdn)
|
||||||
// first check if there is an existing notification for this.
|
|
||||||
nID := fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain)
|
|
||||||
nTTL := 15 * time.Second
|
|
||||||
n := notifications.Get(nID)
|
|
||||||
if n != nil {
|
|
||||||
// we were not here first, only get verdict, do not make changes
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-all", "permit-distinct":
|
|
||||||
comm.Accept("permitted by user")
|
|
||||||
default:
|
|
||||||
comm.Deny("denied by user")
|
|
||||||
}
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
comm.SetReason("user did not respond to prompt")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new notification
|
|
||||||
n = (¬ifications.Notification{
|
|
||||||
ID: nID,
|
|
||||||
Message: fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain),
|
|
||||||
Type: notifications.Prompt,
|
|
||||||
AvailableActions: []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-all",
|
|
||||||
Text: fmt.Sprintf("Permit all %s", comm.Domain),
|
|
||||||
},
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-distinct",
|
|
||||||
Text: fmt.Sprintf("Permit %s", comm.Domain),
|
|
||||||
},
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "deny",
|
|
||||||
Text: "Deny",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Expires: time.Now().Add(nTTL).Unix(),
|
|
||||||
}).Save()
|
|
||||||
|
|
||||||
// react
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
n.Cancel()
|
|
||||||
|
|
||||||
new := &profile.EndpointPermission{
|
|
||||||
Type: profile.EptDomain,
|
|
||||||
Value: comm.Domain,
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-all":
|
|
||||||
new.Value = "." + new.Value
|
|
||||||
case "permit-distinct":
|
|
||||||
// everything already set
|
|
||||||
default:
|
|
||||||
// deny
|
|
||||||
new.Permit = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if new.Permit {
|
|
||||||
log.Infof("firewall: user permitted communication %s -> %s", comm.Process(), new.Value)
|
|
||||||
comm.Accept("permitted by user")
|
|
||||||
} else {
|
|
||||||
log.Infof("firewall: user denied communication %s -> %s", comm.Process(), new.Value)
|
|
||||||
comm.Deny("denied by user")
|
|
||||||
}
|
|
||||||
|
|
||||||
profileSet.Lock()
|
|
||||||
defer profileSet.Unlock()
|
|
||||||
userProfile := profileSet.UserProfile()
|
|
||||||
userProfile.Lock()
|
|
||||||
defer userProfile.Unlock()
|
|
||||||
|
|
||||||
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
|
||||||
go userProfile.Save("")
|
|
||||||
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
n.Cancel()
|
|
||||||
comm.SetReason("user did not respond to prompt")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterDNSResponse filters a dns response according to the application profile and settings.
|
// FilterDNSResponse filters a dns response according to the application profile and settings.
|
||||||
|
@ -573,134 +485,8 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// first check if there is an existing notification for this.
|
// prompt
|
||||||
var nID string
|
prompt(comm, link, pkt, fqdn)
|
||||||
switch {
|
|
||||||
case comm.Direction:
|
|
||||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s-%d-%d", comm.Process().Pid, comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
case fqdn == "":
|
|
||||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s-%d-%d", comm.Process().Pid, comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
default:
|
|
||||||
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s-%d-%d", comm.Process().Pid, comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
}
|
|
||||||
nTTL := 15 * time.Second
|
|
||||||
n := notifications.Get(nID)
|
|
||||||
|
|
||||||
if n != nil {
|
|
||||||
// we were not here first, only get verdict, do not make changes
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-domain-all", "permit-domain-distinct", "permit-ip", "permit-ip-incoming":
|
|
||||||
link.Accept("permitted by user")
|
|
||||||
default:
|
|
||||||
link.Deny("denied by user")
|
|
||||||
}
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
link.Deny("user did not respond to prompt")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new notification
|
|
||||||
n = (¬ifications.Notification{
|
|
||||||
ID: nID,
|
|
||||||
Type: notifications.Prompt,
|
|
||||||
Expires: time.Now().Add(nTTL).Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case comm.Direction:
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", comm.Process(), remoteIP, protocol, dstPort)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-ip-incoming",
|
|
||||||
Text: fmt.Sprintf("Permit serving to %s", remoteIP),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
case fqdn == "":
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", comm.Process(), remoteIP, protocol, dstPort)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-ip",
|
|
||||||
Text: fmt.Sprintf("Permit %s", remoteIP),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, remoteIP, protocol, dstPort)
|
|
||||||
n.AvailableActions = []*notifications.Action{
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-domain-all",
|
|
||||||
Text: fmt.Sprintf("Permit all %s", comm.Domain),
|
|
||||||
},
|
|
||||||
¬ifications.Action{
|
|
||||||
ID: "permit-domain-distinct",
|
|
||||||
Text: fmt.Sprintf("Permit %s", comm.Domain),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n.AvailableActions = append(n.AvailableActions, ¬ifications.Action{
|
|
||||||
ID: "deny",
|
|
||||||
Text: "deny",
|
|
||||||
})
|
|
||||||
n.Save()
|
|
||||||
|
|
||||||
// react
|
|
||||||
select {
|
|
||||||
case promptResponse := <-n.Response():
|
|
||||||
n.Cancel()
|
|
||||||
|
|
||||||
new := &profile.EndpointPermission{
|
|
||||||
Type: profile.EptDomain,
|
|
||||||
Value: comm.Domain,
|
|
||||||
Permit: true,
|
|
||||||
Created: time.Now().Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch promptResponse {
|
|
||||||
case "permit-domain-all":
|
|
||||||
new.Value = "." + new.Value
|
|
||||||
case "permit-domain-distinct":
|
|
||||||
// everything already set
|
|
||||||
case "permit-ip", "permit-ip-incoming":
|
|
||||||
if pkt.Info().Version == packet.IPv4 {
|
|
||||||
new.Type = profile.EptIPv4
|
|
||||||
} else {
|
|
||||||
new.Type = profile.EptIPv6
|
|
||||||
}
|
|
||||||
new.Value = remoteIP.String()
|
|
||||||
default:
|
|
||||||
// deny
|
|
||||||
new.Permit = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if new.Permit {
|
|
||||||
log.Infof("firewall: user permitted link %s -> %s", comm.Process(), new.Value)
|
|
||||||
link.Accept("permitted by user")
|
|
||||||
} else {
|
|
||||||
log.Infof("firewall: user denied link %s -> %s", comm.Process(), new.Value)
|
|
||||||
link.Deny("denied by user")
|
|
||||||
}
|
|
||||||
|
|
||||||
profileSet.Lock()
|
|
||||||
defer profileSet.Unlock()
|
|
||||||
userProfile := profileSet.UserProfile()
|
|
||||||
userProfile.Lock()
|
|
||||||
defer userProfile.Unlock()
|
|
||||||
|
|
||||||
if promptResponse == "permit-ip-incoming" {
|
|
||||||
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new)
|
|
||||||
} else {
|
|
||||||
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
|
||||||
}
|
|
||||||
go userProfile.Save("")
|
|
||||||
|
|
||||||
case <-time.After(nTTL):
|
|
||||||
n.Cancel()
|
|
||||||
link.Deny("user did not respond to prompt")
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
|
func checkRelation(comm *network.Communication, fqdn string) (related bool) {
|
||||||
|
|
194
firewall/prompt.go
Normal file
194
firewall/prompt.go
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/portbase/log"
|
||||||
|
"github.com/safing/portbase/notifications"
|
||||||
|
"github.com/safing/portmaster/network"
|
||||||
|
"github.com/safing/portmaster/network/packet"
|
||||||
|
"github.com/safing/portmaster/profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// notification action IDs
|
||||||
|
permitDomainAll = "permit-domain-all"
|
||||||
|
permitDomainDistinct = "permit-domain-distinct"
|
||||||
|
denyDomainAll = "deny-domain-all"
|
||||||
|
denyDomainDistinct = "deny-domain-distinct"
|
||||||
|
|
||||||
|
permitIP = "permit-ip"
|
||||||
|
denyIP = "deny-ip"
|
||||||
|
permitServingIP = "permit-serving-ip"
|
||||||
|
denyServingIP = "deny-serving-ip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prompt(comm *network.Communication, link *network.Link, pkt packet.Packet, fqdn string) {
|
||||||
|
nTTL := time.Duration(promptTimeout()) * time.Second
|
||||||
|
|
||||||
|
// first check if there is an existing notification for this.
|
||||||
|
// build notification ID
|
||||||
|
var nID string
|
||||||
|
switch {
|
||||||
|
case comm.Direction, fqdn == "": // connection to/from IP
|
||||||
|
if pkt == nil {
|
||||||
|
log.Error("firewall: could not prompt for incoming/direct connection: missing pkt")
|
||||||
|
if link != nil {
|
||||||
|
link.Deny("internal error")
|
||||||
|
} else {
|
||||||
|
comm.Deny("internal error")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nID = fmt.Sprintf("firewall-prompt-%d-%s-%s", comm.Process().Pid, comm.Domain, pkt.Info().RemoteIP)
|
||||||
|
default: // connection to domain
|
||||||
|
nID = fmt.Sprintf("firewall-prompt-%d-%s", comm.Process().Pid, comm.Domain)
|
||||||
|
}
|
||||||
|
n := notifications.Get(nID)
|
||||||
|
saveResponse := true
|
||||||
|
|
||||||
|
if n != nil {
|
||||||
|
// update with new expiry
|
||||||
|
n.Update(time.Now().Add(nTTL).Unix())
|
||||||
|
// do not save response to profile
|
||||||
|
saveResponse = false
|
||||||
|
} else {
|
||||||
|
// create new notification
|
||||||
|
n = (¬ifications.Notification{
|
||||||
|
ID: nID,
|
||||||
|
Type: notifications.Prompt,
|
||||||
|
Expires: time.Now().Add(nTTL).Unix(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// add message and actions
|
||||||
|
switch {
|
||||||
|
case comm.Direction: // incoming
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().LocalPort())
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitServingIP,
|
||||||
|
Text: "Permit",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: denyServingIP,
|
||||||
|
Text: "Deny",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case fqdn == "": // direct connection
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to connect to %s (on %d/%d)", comm.Process(), pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitIP,
|
||||||
|
Text: "Permit",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: denyIP,
|
||||||
|
Text: "Deny",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
default: // connection to domain
|
||||||
|
if pkt != nil {
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", comm.Process(), comm.Domain, pkt.Info().RemoteIP(), pkt.Info().Protocol, pkt.Info().RemotePort())
|
||||||
|
} else {
|
||||||
|
n.Message = fmt.Sprintf("Application %s wants to connect to %s", comm.Process(), comm.Domain)
|
||||||
|
}
|
||||||
|
n.AvailableActions = []*notifications.Action{
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitDomainAll,
|
||||||
|
Text: "Permit all",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: permitDomainDistinct,
|
||||||
|
Text: "Permit",
|
||||||
|
},
|
||||||
|
¬ifications.Action{
|
||||||
|
ID: denyDomainDistinct,
|
||||||
|
Text: "Deny",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// save new notification
|
||||||
|
n.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for response/timeout
|
||||||
|
select {
|
||||||
|
case promptResponse := <-n.Response():
|
||||||
|
switch promptResponse {
|
||||||
|
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
|
||||||
|
if link != nil {
|
||||||
|
link.Accept("permitted by user")
|
||||||
|
} else {
|
||||||
|
comm.Accept("permitted by user")
|
||||||
|
}
|
||||||
|
default: // deny
|
||||||
|
if link != nil {
|
||||||
|
link.Accept("denied by user")
|
||||||
|
} else {
|
||||||
|
comm.Accept("denied by user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// end here if we won't save the response to the profile
|
||||||
|
if !saveResponse {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
new := &profile.EndpointPermission{
|
||||||
|
Type: profile.EptDomain,
|
||||||
|
Value: comm.Domain,
|
||||||
|
Permit: false,
|
||||||
|
Created: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// permission type
|
||||||
|
switch promptResponse {
|
||||||
|
case permitDomainAll, denyDomainAll:
|
||||||
|
new.Value = "." + new.Value
|
||||||
|
case permitIP, permitServingIP, denyIP, denyServingIP:
|
||||||
|
if pkt == nil {
|
||||||
|
log.Warningf("firewall: received invalid prompt response: %s for %s", promptResponse, comm.Domain)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if pkt.Info().Version == packet.IPv4 {
|
||||||
|
new.Type = profile.EptIPv4
|
||||||
|
} else {
|
||||||
|
new.Type = profile.EptIPv6
|
||||||
|
}
|
||||||
|
new.Value = pkt.Info().RemoteIP().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// permission verdict
|
||||||
|
switch promptResponse {
|
||||||
|
case permitDomainAll, permitDomainDistinct, permitIP, permitServingIP:
|
||||||
|
new.Permit = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user profile
|
||||||
|
profileSet := comm.Process().ProfileSet()
|
||||||
|
profileSet.Lock()
|
||||||
|
defer profileSet.Unlock()
|
||||||
|
userProfile := profileSet.UserProfile()
|
||||||
|
userProfile.Lock()
|
||||||
|
defer userProfile.Unlock()
|
||||||
|
|
||||||
|
// add to correct list
|
||||||
|
switch promptResponse {
|
||||||
|
case permitServingIP, denyServingIP:
|
||||||
|
userProfile.ServiceEndpoints = append(userProfile.ServiceEndpoints, new)
|
||||||
|
default:
|
||||||
|
userProfile.Endpoints = append(userProfile.Endpoints, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save!
|
||||||
|
go userProfile.Save("")
|
||||||
|
|
||||||
|
case <-n.Expired():
|
||||||
|
if link != nil {
|
||||||
|
link.Accept("no response to prompt")
|
||||||
|
} else {
|
||||||
|
comm.Accept("no response to prompt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue