Revamped verdict handling

This commit is contained in:
Patrick Pacher 2023-11-16 12:52:39 +01:00
parent f2839c274a
commit 923ce2aa24
No known key found for this signature in database
GPG key ID: E8CD2DA160925A6D
13 changed files with 55 additions and 154 deletions

View file

@ -177,15 +177,6 @@ func FilterResolvedDNS(
return rrCache return rrCache
} }
// Finalize verdict.
defer func() {
// Reset from previous filtering.
conn.Verdict.Active = network.VerdictUndecided
conn.Verdict.Worst = network.VerdictUndecided
// Update all values again.
finalizeVerdict(conn)
}()
// special grant for connectivity domains // special grant for connectivity domains
if checkConnectivityDomain(ctx, conn, layeredProfile, nil) { if checkConnectivityDomain(ctx, conn, layeredProfile, nil) {
// returns true if check triggered // returns true if check triggered
@ -197,7 +188,7 @@ func FilterResolvedDNS(
// Filter dns records and return if the query is blocked. // Filter dns records and return if the query is blocked.
rrCache = filterDNSResponse(ctx, conn, layeredProfile, rrCache, sysResolver) rrCache = filterDNSResponse(ctx, conn, layeredProfile, rrCache, sysResolver)
if conn.Verdict.Active == network.VerdictBlock { if conn.Verdict == network.VerdictBlock {
return rrCache return rrCache
} }

View file

@ -64,7 +64,7 @@ func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict
} }
// check if the active verdict is already past the inspection criteria. // check if the active verdict is already past the inspection criteria.
if conn.Verdict.Active > inspectVerdicts[key] { if conn.Verdict > inspectVerdicts[key] {
activeInspectors[key] = true activeInspectors[key] = true
continue continue
} }
@ -86,11 +86,11 @@ func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict
continueInspection = true continueInspection = true
case BLOCK_CONN: case BLOCK_CONN:
conn.SetVerdict(network.VerdictBlock, "", "", nil) conn.SetVerdict(network.VerdictBlock, "", "", nil)
verdict = conn.Verdict.Active verdict = conn.Verdict
activeInspectors[key] = true activeInspectors[key] = true
case DROP_CONN: case DROP_CONN:
conn.SetVerdict(network.VerdictDrop, "", "", nil) conn.SetVerdict(network.VerdictDrop, "", "", nil)
verdict = conn.Verdict.Active verdict = conn.Verdict
activeInspectors[key] = true activeInspectors[key] = true
case STOP_INSPECTING: case STOP_INSPECTING:
activeInspectors[key] = true activeInspectors[key] = true

View file

@ -254,7 +254,7 @@ func UpdateVerdict(conn *network.Connection) error {
localPort: conn.LocalPort, localPort: conn.LocalPort,
remoteIP: ipAddressToArray(conn.Entity.IP, isIpv6 == 1), remoteIP: ipAddressToArray(conn.Entity.IP, isIpv6 == 1),
remotePort: conn.Entity.Port, remotePort: conn.Entity.Port,
verdict: uint8(conn.Verdict.Active), verdict: uint8(conn.Verdict),
} }
// Make driver request // Make driver request

View file

@ -103,7 +103,7 @@ func decideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
case profile.DefaultActionAsk: case profile.DefaultActionAsk:
// Only prompt if there has not been a decision already. // Only prompt if there has not been a decision already.
// This prevents prompts from being created when re-evaluating connections. // This prevents prompts from being created when re-evaluating connections.
if conn.Verdict.Firewall == network.VerdictUndecided { if conn.Verdict == network.VerdictUndecided {
prompt(ctx, conn) prompt(ctx, conn)
} }
default: default:

View file

@ -22,7 +22,6 @@ import (
"github.com/safing/portmaster/network" "github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils" "github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet" "github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/network/reference"
"github.com/safing/portmaster/process" "github.com/safing/portmaster/process"
"github.com/safing/spn/access" "github.com/safing/spn/access"
) )
@ -132,13 +131,13 @@ func resetConnectionVerdict(ctx context.Context, conn *network.Connection) (verd
} }
tracer.Debugf("filter: re-evaluating verdict of %s", conn) tracer.Debugf("filter: re-evaluating verdict of %s", conn)
previousVerdict := conn.Verdict.Firewall previousVerdict := conn.Verdict
// Apply privacy filter and check tunneling. // Apply privacy filter and check tunneling.
FilterConnection(ctx, conn, nil, true, true) FilterConnection(ctx, conn, nil, true, true)
// Stop existing SPN tunnel if not needed anymore. // Stop existing SPN tunnel if not needed anymore.
if conn.Verdict.Active != network.VerdictRerouteToTunnel && conn.TunnelContext != nil { if conn.Verdict != network.VerdictRerouteToTunnel && conn.TunnelContext != nil {
err := conn.TunnelContext.StopTunnel() err := conn.TunnelContext.StopTunnel()
if err != nil { if err != nil {
tracer.Debugf("filter: failed to stopped unneeded tunnel: %s", err) tracer.Debugf("filter: failed to stopped unneeded tunnel: %s", err)
@ -146,7 +145,11 @@ func resetConnectionVerdict(ctx context.Context, conn *network.Connection) (verd
} }
// Save if verdict changed. // Save if verdict changed.
if conn.Verdict.Firewall != previousVerdict { if conn.Verdict != previousVerdict {
err := interception.UpdateVerdictOfConnection(conn)
if err != nil {
log.Debugf("filter: failed to update connection verdict: %s", err)
}
conn.Save() conn.Save()
tracer.Infof("filter: verdict of connection %s changed from %s to %s", conn, previousVerdict.Verb(), conn.VerdictVerb()) tracer.Infof("filter: verdict of connection %s changed from %s to %s", conn, previousVerdict.Verb(), conn.VerdictVerb())
@ -368,16 +371,17 @@ func fastTrackHandler(conn *network.Connection, pkt packet.Packet) {
fastTrackedVerdict, permanent := fastTrackedPermit(conn, pkt) fastTrackedVerdict, permanent := fastTrackedPermit(conn, pkt)
if fastTrackedVerdict != network.VerdictUndecided { if fastTrackedVerdict != network.VerdictUndecided {
// Set verdict on connection. // Set verdict on connection.
conn.Verdict.Active = fastTrackedVerdict conn.Verdict = fastTrackedVerdict
conn.Verdict.Firewall = fastTrackedVerdict
// Apply verdict to (real) packet. // Apply verdict to (real) packet.
if !pkt.InfoOnly() { if !pkt.InfoOnly() {
issueVerdict(conn, pkt, fastTrackedVerdict, permanent) issueVerdict(conn, pkt, fastTrackedVerdict, permanent)
} }
// Stop handler if permanent. // Stop handler if permanent.
if permanent { if permanent {
conn.SetVerdict(fastTrackedVerdict, "fast-tracked", "", nil) conn.SetVerdict(fastTrackedVerdict, "fast-tracked", "", nil)
conn.Verdict.Worst = fastTrackedVerdict
// Do not finalize verdict, as we are missing necessary data. // Do not finalize verdict, as we are missing necessary data.
conn.StopFirewallHandler() conn.StopFirewallHandler()
} }
@ -447,7 +451,7 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
// End directly, as no other processing is necessary. // End directly, as no other processing is necessary.
conn.StopFirewallHandler() conn.StopFirewallHandler()
finalizeVerdict(conn)
issueVerdict(conn, pkt, 0, true) issueVerdict(conn, pkt, 0, true)
return return
} }
@ -504,19 +508,17 @@ func FilterConnection(ctx context.Context, conn *network.Connection, pkt packet.
checkTunneling(ctx, conn) checkTunneling(ctx, conn)
} }
// Handle verdict records and transitions.
finalizeVerdict(conn)
// Request tunneling if no tunnel is set and connection should be tunneled. // Request tunneling if no tunnel is set and connection should be tunneled.
if conn.Verdict.Active == network.VerdictRerouteToTunnel && if conn.Verdict == network.VerdictRerouteToTunnel &&
conn.TunnelContext == nil { conn.TunnelContext == nil {
err := requestTunneling(ctx, conn) err := requestTunneling(ctx, conn)
if err != nil { if err == nil {
conn.ConnectionEstablished = true
} else {
// Set connection to failed, but keep tunneling data. // Set connection to failed, but keep tunneling data.
// The tunneling data makes connection easy to recognize as a failed SPN // The tunneling data makes connection easy to recognize as a failed SPN
// connection and the data will help with debugging and displaying in the UI. // connection and the data will help with debugging and displaying in the UI.
conn.Failed(fmt.Sprintf("failed to request tunneling: %s", err), "") conn.Failed(fmt.Sprintf("failed to request tunneling: %s", err), "")
finalizeVerdict(conn)
} }
} }
} }
@ -563,8 +565,8 @@ func issueVerdict(conn *network.Connection, pkt packet.Packet, verdict network.V
} }
// do not allow to circumvent decision: e.g. to ACCEPT packets from a DROP-ed connection // do not allow to circumvent decision: e.g. to ACCEPT packets from a DROP-ed connection
if verdict < conn.Verdict.Active { if verdict < conn.Verdict {
verdict = conn.Verdict.Active verdict = conn.Verdict
} }
var err error var err error
@ -622,53 +624,6 @@ var verdictRating = []network.Verdict{
network.VerdictUndecided, network.VerdictUndecided,
} }
func finalizeVerdict(conn *network.Connection) {
// Update worst verdict at the end.
defer func() {
for _, worstVerdict := range verdictRating {
if conn.Verdict.Firewall == worstVerdict {
conn.Verdict.Worst = worstVerdict
}
}
}()
// Check for non-applicable verdicts.
// The earlier and clearer we do this, the better.
switch conn.Verdict.Firewall { //nolint:exhaustive
case network.VerdictUndecided, network.VerdictUndeterminable, network.VerdictFailed:
if conn.Inbound {
conn.Verdict.Active = network.VerdictDrop
} else {
conn.Verdict.Active = network.VerdictBlock
}
return
}
// Apply firewall verdict to active verdict.
switch {
case conn.Verdict.Active == network.VerdictUndecided:
// Apply first verdict without change.
conn.Verdict.Active = conn.Verdict.Firewall
case conn.Verdict.Worst == network.VerdictBlock ||
conn.Verdict.Worst == network.VerdictDrop ||
conn.Verdict.Worst == network.VerdictFailed ||
conn.Verdict.Worst == network.VerdictUndeterminable:
// Always allow to change verdict from any real initial/worst non-allowed state.
// Note: This check needs to happen before updating the Worst verdict.
conn.Verdict.Active = conn.Verdict.Firewall
case reference.IsPacketProtocol(conn.Entity.Protocol):
// For known packet protocols, apply firewall verdict unchanged.
conn.Verdict.Active = conn.Verdict.Firewall
case conn.Verdict.Active != conn.Verdict.Firewall:
// For all other protocols (most notably, stream protocols), always block after the first change.
// Block in both directions, as there is a live connection, which we want to actively kill.
conn.Verdict.Active = network.VerdictBlock
}
}
// func tunnelHandler(pkt packet.Packet) { // func tunnelHandler(pkt packet.Packet) {
// tunnelInfo := GetTunnelInfo(pkt.Info().Dst) // tunnelInfo := GetTunnelInfo(pkt.Info().Dst)
// if tunnelInfo == nil { // if tunnelInfo == nil {

View file

@ -31,7 +31,7 @@ func checkTunneling(ctx context.Context, conn *network.Connection) {
case conn.Inbound: case conn.Inbound:
// Can't tunnel incoming connections. // Can't tunnel incoming connections.
return return
case conn.Verdict.Firewall != network.VerdictAccept: case conn.Verdict != network.VerdictAccept:
// Connection will be blocked. // Connection will be blocked.
return return
case conn.IPProtocol != packet.TCP && conn.IPProtocol != packet.UDP: case conn.IPProtocol != packet.TCP && conn.IPProtocol != packet.UDP:

View file

@ -199,7 +199,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
} }
} }
switch conn.Verdict.Active { switch conn.Verdict {
// We immediately save blocked, dropped or failed verdicts so // We immediately save blocked, dropped or failed verdicts so
// they pop up in the UI. // they pop up in the UI.
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel: case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel:
@ -245,7 +245,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
} }
// Check if there is a Verdict to act upon. // Check if there is a Verdict to act upon.
switch conn.Verdict.Active { //nolint:exhaustive // Only checking for specific values. switch conn.Verdict { //nolint:exhaustive // Only checking for specific values.
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed:
tracer.Infof( tracer.Infof(
"nameserver: returning %s response for %s to %s", "nameserver: returning %s response for %s to %s",
@ -325,7 +325,7 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
} }
// Check if there is a Verdict to act upon. // Check if there is a Verdict to act upon.
switch conn.Verdict.Active { //nolint:exhaustive // Only checking for specific values. switch conn.Verdict { //nolint:exhaustive // Only checking for specific values.
case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed: case network.VerdictBlock, network.VerdictDrop, network.VerdictFailed:
tracer.Infof( tracer.Infof(
"nameserver: returning %s response for %s to %s", "nameserver: returning %s response for %s to %s",

View file

@ -183,9 +183,7 @@ func convertConnection(conn *network.Connection) (*Conn, error) {
IPProtocol: conn.IPProtocol, IPProtocol: conn.IPProtocol,
LocalIP: conn.LocalIP.String(), LocalIP: conn.LocalIP.String(),
LocalPort: conn.LocalPort, LocalPort: conn.LocalPort,
FirewallVerdict: conn.Verdict.Firewall, ActiveVerdict: conn.Verdict,
ActiveVerdict: conn.Verdict.Active,
WorstVerdict: conn.Verdict.Worst,
Started: time.Unix(conn.Started, 0), Started: time.Unix(conn.Started, 0),
Tunneled: conn.Tunneled, Tunneled: conn.Tunneled,
Encrypted: conn.Encrypted, Encrypted: conn.Encrypted,
@ -207,16 +205,7 @@ func convertConnection(conn *network.Connection) (*Conn, error) {
c.Type = "" c.Type = ""
} }
switch conn.Verdict.Firewall { c.Allowed = &conn.ConnectionEstablished
case network.VerdictAccept, network.VerdictRerouteToNameserver, network.VerdictRerouteToTunnel:
accepted := true
c.Allowed = &accepted
case network.VerdictBlock, network.VerdictDrop:
allowed := false
c.Allowed = &allowed
case network.VerdictUndecided, network.VerdictUndeterminable, network.VerdictFailed:
c.Allowed = nil
}
if conn.Ended > 0 { if conn.Ended > 0 {
ended := time.Unix(conn.Ended, 0) ended := time.Unix(conn.Ended, 0)

View file

@ -139,8 +139,8 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) {
debugConns []*Connection debugConns []*Connection
accepted int accepted int
total int total int
transitioning int
) )
for maybeConn := range it.Next { for maybeConn := range it.Next {
// Switch to correct type. // Switch to correct type.
conn, ok := maybeConn.(*Connection) conn, ok := maybeConn.(*Connection)
@ -169,15 +169,13 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) {
// Count. // Count.
total++ total++
switch conn.Verdict.Firewall { //nolint:exhaustive switch conn.Verdict { //nolint:exhaustive
case VerdictAccept, case VerdictAccept,
VerdictRerouteToNameserver, VerdictRerouteToNameserver,
VerdictRerouteToTunnel: VerdictRerouteToTunnel:
accepted++ accepted++
} }
if conn.Verdict.Active != conn.Verdict.Firewall {
transitioning++
}
// Add to list. // Add to list.
debugConns = append(debugConns, conn) debugConns = append(debugConns, conn)
@ -186,10 +184,9 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) {
// Add it all. // Add it all.
di.AddSection( di.AddSection(
fmt.Sprintf( fmt.Sprintf(
"Network: %d/%d [~%d] Connections", "Network: %d/%d Connections",
accepted, accepted,
total, total,
transitioning,
), ),
debug.UseCodeSection|debug.AddContentLineBreaks, debug.UseCodeSection|debug.AddContentLineBreaks,
buildNetworkDebugInfoData(debugConns), buildNetworkDebugInfoData(debugConns),

View file

@ -40,15 +40,7 @@ var connectionTestData = []*Connection{
Country: "", Country: "",
ASN: 0, ASN: 0,
}, },
Verdict: struct { Verdict: 2,
Worst Verdict
Active Verdict
Firewall Verdict
}{
Worst: 2,
Active: 2,
Firewall: 2,
},
Reason: Reason{ Reason: Reason{
Msg: "incoming connection blocked by default", Msg: "incoming connection blocked by default",
OptionKey: "filter/serviceEndpoints", OptionKey: "filter/serviceEndpoints",
@ -88,15 +80,7 @@ var connectionTestData = []*Connection{
Country: "DE", Country: "DE",
ASN: 16509, ASN: 16509,
}, },
Verdict: struct { Verdict: 2,
Worst Verdict
Active Verdict
Firewall Verdict
}{
Worst: 2,
Active: 2,
Firewall: 2,
},
Reason: Reason{ Reason: Reason{
Msg: "default permit", Msg: "default permit",
OptionKey: "filter/defaultAction", OptionKey: "filter/defaultAction",
@ -139,15 +123,7 @@ var connectionTestData = []*Connection{
Country: "US", Country: "US",
ASN: 15169, ASN: 15169,
}, },
Verdict: struct { Verdict: 2,
Worst Verdict
Active Verdict
Firewall Verdict
}{
Worst: 2,
Active: 2,
Firewall: 2,
},
Reason: Reason{ Reason: Reason{
Msg: "default permit", Msg: "default permit",
OptionKey: "filter/defaultAction", OptionKey: "filter/defaultAction",

View file

@ -121,17 +121,9 @@ type Connection struct { //nolint:maligned // TODO: fix alignment
// Verdict holds the decisions that are made for a connection // Verdict holds the decisions that are made for a connection
// The verdict may change so any access to it must be guarded by the // The verdict may change so any access to it must be guarded by the
// connection lock. // connection lock.
Verdict struct { Verdict Verdict
// Worst verdict holds the worst verdict that was assigned to this // Whether or not the connection has been established at least once.
// connection from a privacy/security perspective. ConnectionEstablished bool
Worst Verdict
// Active verdict holds the verdict that Portmaster will respond with.
// This is different from the Firewall verdict in order to guarantee proper
// transition between verdicts that need the connection to be re-established.
Active Verdict
// Firewall holds the last (most recent) decision by the firewall.
Firewall Verdict
}
// Reason holds information justifying the verdict, as well as additional // Reason holds information justifying the verdict, as well as additional
// information about the reason. // information about the reason.
// Access to Reason must be guarded by the connection lock. // Access to Reason must be guarded by the connection lock.
@ -722,22 +714,15 @@ func (conn *Connection) SetVerdict(newVerdict Verdict, reason, reasonOptionKey s
return true // TODO: remove return true // TODO: remove
} }
// SetVerdictDirectly sets the firewall verdict. // SetVerdictDirectly sets the verdict.
func (conn *Connection) SetVerdictDirectly(newVerdict Verdict) { func (conn *Connection) SetVerdictDirectly(newVerdict Verdict) {
conn.Verdict.Firewall = newVerdict conn.Verdict = newVerdict
} }
// VerdictVerb returns the verdict as a verb, while taking any special states // VerdictVerb returns the verdict as a verb, while taking any special states
// into account. // into account.
func (conn *Connection) VerdictVerb() string { func (conn *Connection) VerdictVerb() string {
if conn.Verdict.Firewall == conn.Verdict.Active { return conn.Verdict.Verb()
return conn.Verdict.Firewall.Verb()
}
return fmt.Sprintf(
"%s (transitioning to %s)",
conn.Verdict.Active.Verb(),
conn.Verdict.Firewall.Verb(),
)
} }
// DataIsComplete returns whether all information about the connection is // DataIsComplete returns whether all information about the connection is
@ -766,6 +751,14 @@ func (conn *Connection) SaveWhenFinished() {
func (conn *Connection) Save() { func (conn *Connection) Save() {
conn.UpdateMeta() conn.UpdateMeta()
switch conn.Verdict {
case VerdictAccept, VerdictRerouteToNameserver:
conn.ConnectionEstablished = true
case VerdictRerouteToTunnel:
// this is already handled when the connection tunnel has been
// established.
}
// Do not save/update until data is complete. // Do not save/update until data is complete.
if !conn.DataIsComplete() { if !conn.DataIsComplete() {
return return
@ -1003,7 +996,7 @@ func packetHandlerHandleConn(ctx context.Context, conn *Connection, pkt packet.P
switch { switch {
case conn.DataIsComplete(): case conn.DataIsComplete():
tracer.Infof("filter: connection %s %s: %s", conn, conn.VerdictVerb(), conn.Reason.Msg) tracer.Infof("filter: connection %s %s: %s", conn, conn.VerdictVerb(), conn.Reason.Msg)
case conn.Verdict.Firewall != VerdictUndecided: case conn.Verdict != VerdictUndecided:
tracer.Debugf("filter: connection %s fast-tracked", pkt) tracer.Debugf("filter: connection %s fast-tracked", pkt)
default: default:
tracer.Debugf("filter: gathered data on connection %s", conn) tracer.Debugf("filter: gathered data on connection %s", conn)

View file

@ -208,7 +208,7 @@ func writeOpenDNSRequestsToDB() {
// ReplyWithDNS creates a new reply to the given request with the data from the RRCache, and additional informational records. // ReplyWithDNS creates a new reply to the given request with the data from the RRCache, and additional informational records.
func (conn *Connection) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg { func (conn *Connection) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns.Msg {
// Select request responder. // Select request responder.
switch conn.Verdict.Active { switch conn.Verdict {
case VerdictBlock: case VerdictBlock:
return nsutil.BlockIP().ReplyWithDNS(ctx, request) return nsutil.BlockIP().ReplyWithDNS(ctx, request)
case VerdictDrop: case VerdictDrop:
@ -229,7 +229,7 @@ func (conn *Connection) ReplyWithDNS(ctx context.Context, request *dns.Msg) *dns
func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns.RR { func (conn *Connection) GetExtraRRs(ctx context.Context, request *dns.Msg) []dns.RR {
// Select level to add the verdict record with. // Select level to add the verdict record with.
var level log.Severity var level log.Severity
switch conn.Verdict.Active { switch conn.Verdict {
case VerdictFailed: case VerdictFailed:
level = log.ErrorLevel level = log.ErrorLevel
case VerdictUndecided, VerdictUndeterminable, case VerdictUndecided, VerdictUndeterminable,

View file

@ -140,7 +140,7 @@ func (conn *Connection) addToMetrics() {
} }
// Check the verdict. // Check the verdict.
switch conn.Verdict.Firewall { //nolint:exhaustive // Not critical. switch conn.Verdict { //nolint:exhaustive // Not critical.
case VerdictBlock, VerdictDrop: case VerdictBlock, VerdictDrop:
blockedOutConnCounter.Inc() blockedOutConnCounter.Inc()
conn.addedToMetrics = true conn.addedToMetrics = true