safing-portmaster/firewall/interception.go

712 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package firewall
import (
"context"
"errors"
"fmt"
"net"
"os"
"sync/atomic"
"time"
"github.com/google/gopacket/layers"
"github.com/tevino/abool"
"golang.org/x/sync/singleflight"
"github.com/safing/portbase/config"
"github.com/safing/portbase/log"
"github.com/safing/portbase/modules"
"github.com/safing/portmaster/compat"
_ "github.com/safing/portmaster/core/base"
"github.com/safing/portmaster/firewall/inspection"
"github.com/safing/portmaster/firewall/interception"
"github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/netutils"
"github.com/safing/portmaster/network/packet"
"github.com/safing/portmaster/network/reference"
)
var (
interceptionModule *modules.Module
nameserverIPMatcher func(ip net.IP) bool
nameserverIPMatcherSet = abool.New()
nameserverIPMatcherReady = abool.New()
packetsAccepted = new(uint64)
packetsBlocked = new(uint64)
packetsDropped = new(uint64)
packetsFailed = new(uint64)
blockedIPv4 = net.IPv4(0, 0, 0, 17)
blockedIPv6 = net.ParseIP("::17")
ownPID = os.Getpid()
)
// Config variables for interception module.
var (
devMode config.BoolOption
apiListenAddress config.StringOption
)
const (
configChangeEvent = "config change"
profileConfigChangeEvent = "profile config change"
onSPNConnectEvent = "spn connect"
)
func init() {
// TODO: Move interception module to own package (dir).
interceptionModule = modules.Register("interception", interceptionPrep, interceptionStart, interceptionStop, "base", "updates", "network", "notifications", "profiles")
network.SetDefaultFirewallHandler(defaultHandler)
}
func interceptionPrep() error {
// Reset connections every time configuration changes
// this will be triggered on spn enable/disable
err := interceptionModule.RegisterEventHook(
"config",
configChangeEvent,
"reset connection verdicts",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
return nil
},
)
if err != nil {
log.Errorf("interception: failed registering event hook: %s", err)
}
// Reset connections every time profile changes
err = interceptionModule.RegisterEventHook(
"profiles",
profileConfigChangeEvent,
"reset connection verdicts",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
return nil
},
)
if err != nil {
log.Errorf("failed registering event hook: %s", err)
}
// Reset connections when spn is connected
// connect and disconnecting is triggered on config change event but connecting takеs more time
err = interceptionModule.RegisterEventHook(
"captain",
onSPNConnectEvent,
"reset connection verdicts",
func(ctx context.Context, _ interface{}) error {
resetAllConnectionVerdicts()
return nil
},
)
if err != nil {
log.Errorf("failed registering event hook: %s", err)
}
if err := registerConfig(); err != nil {
return err
}
return prepAPIAuth()
}
func resetAllConnectionVerdicts() {
// Resetting will force all the connection to be evaluated by the firewall again
// this will set new verdicts if configuration was update or spn has been disabled or enabled.
log.Info("interception: marking all connections for re-evaluation")
// reset all connection firewall handlers. This will tell the master to rerun the firewall checks.
// for _, conn := range network.GetAllConnections() {
// isSPNConnection := captain.IsExcepted(conn.Entity.IP) && conn.Process().Pid == ownPID
// // mark all non SPN connections to be processed by the firewall.
// if !isSPNConnection {
// conn.Lock()
// conn.SetFirewallHandler(initialHandler)
// // Don't keep the previous tunneled value.
// conn.Tunneled = false
// // Reset entity if it exists.
// if conn.Entity != nil {
// conn.Entity.ResetLists()
// }
// conn.Unlock()
// }
// }
// Create tracing context.
ctx, tracer := log.AddTracer(context.Background())
defer tracer.Submit()
for _, conn := range network.GetAllConnections() {
func() {
conn.Lock()
defer conn.Unlock()
// Skip internal connections:
// - Pre-authenticated connections from Portmaster
// - Redirected DNS requests
// - SPN Uplink to Home Hub
if conn.Internal {
log.Tracef("skipping internal connection %s", conn)
return
}
log.Tracer(ctx).Debugf("filter: re-evaluating verdict of %s", conn)
previousVerdict := conn.Verdict.Firewall
// Apply privacy filter and check tunneling.
filterConnection(ctx, conn, nil)
// Save if verdict changed.
if conn.Verdict.Firewall != previousVerdict {
conn.Save()
tracer.Infof("filter: verdict of connection %s changed from %s to %s", conn, previousVerdict.Verb(), conn.VerdictVerb())
} else {
tracer.Tracef("filter: verdict to connection %s unchanged at %s", conn, conn.VerdictVerb())
}
}()
}
err := interception.ResetVerdictOfAllConnections()
if err != nil {
log.Errorf("interception: failed to remove persistent verdicts: %s", err)
}
}
func interceptionStart() error {
getConfig()
if err := registerMetrics(); err != nil {
return err
}
startAPIAuth()
interceptionModule.StartWorker("stat logger", statLogger)
interceptionModule.StartWorker("packet handler", packetHandler)
return interception.Start()
}
func interceptionStop() error {
return interception.Stop()
}
// SetNameserverIPMatcher sets a function that is used to match the internal
// nameserver IP(s). Can only bet set once.
func SetNameserverIPMatcher(fn func(ip net.IP) bool) error {
if !nameserverIPMatcherSet.SetToIf(false, true) {
return errors.New("nameserver IP matcher already set")
}
nameserverIPMatcher = fn
nameserverIPMatcherReady.Set()
return nil
}
func handlePacket(ctx context.Context, pkt packet.Packet) {
// Record metrics.
startTime := time.Now()
defer packetHandlingHistogram.UpdateDuration(startTime)
if fastTrackedPermit(pkt) {
return
}
// Add context tracer and set context on packet.
traceCtx, tracer := log.AddTracer(ctx)
if tracer != nil {
// The trace is submitted in `network.Connection.packetHandler()`.
tracer.Tracef("filter: handling packet: %s", pkt)
}
pkt.SetCtx(traceCtx)
// Get connection of packet.
conn, err := getConnection(pkt)
if err != nil {
tracer.Errorf("filter: packet %s dropped: %s", pkt, err)
_ = pkt.Drop()
return
}
// handle packet
conn.HandlePacket(pkt)
}
var getConnectionSingleInflight singleflight.Group
func getConnection(pkt packet.Packet) (*network.Connection, error) {
created := false
// Create or get connection in single inflight lock in order to prevent duplicates.
newConn, err, shared := getConnectionSingleInflight.Do(pkt.GetConnectionID(), func() (interface{}, error) {
// First, check for an existing connection.
conn, ok := network.GetConnection(pkt.GetConnectionID())
if ok {
return conn, nil
}
// Else create new one from the packet.
conn = network.NewConnectionFromFirstPacket(pkt)
conn.Lock()
defer conn.Unlock()
conn.SetFirewallHandler(initialHandler)
created = true
return conn, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get connection: %w", err)
}
if newConn == nil {
return nil, errors.New("connection getter returned nil")
}
// Transform and log result.
conn := newConn.(*network.Connection) //nolint:forcetypeassert // Can only be a *network.Connection.
sharedIndicator := ""
if shared {
sharedIndicator = " (shared)"
}
if created {
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s%s", conn.ID, sharedIndicator)
} else {
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s%s", conn.ID, sharedIndicator)
}
return conn, nil
}
// fastTrackedPermit quickly permits certain network critical or internal connections.
func fastTrackedPermit(pkt packet.Packet) (handled bool) {
meta := pkt.Info()
// Check if packed was already fast-tracked by the OS integration.
if pkt.FastTrackedByIntegration() {
log.Debugf("filter: fast-tracked by OS integration: %s", pkt)
return true
}
// Check if connection was already blocked.
if meta.Dst.Equal(blockedIPv4) || meta.Dst.Equal(blockedIPv6) {
_ = pkt.PermanentBlock()
return true
}
// Some programs do a network self-check where they connects to the same
// IP/Port to test network capabilities.
// Eg. dig: https://gitlab.isc.org/isc-projects/bind9/-/issues/1140
if meta.SrcPort == meta.DstPort &&
meta.Src.Equal(meta.Dst) {
log.Debugf("filter: fast-track network self-check: %s", pkt)
_ = pkt.PermanentAccept()
return true
}
switch meta.Protocol { //nolint:exhaustive // Checking for specific values only.
case packet.ICMP, packet.ICMPv6:
// Load packet data.
err := pkt.LoadPacketData()
if err != nil {
log.Debugf("filter: failed to load ICMP packet data: %s", err)
_ = pkt.PermanentAccept()
return true
}
// Submit to ICMP listener.
submitted := netenv.SubmitPacketToICMPListener(pkt)
if submitted {
// If the packet was submitted to the listener, we must not do a
// permanent accept, because then we won't see any future packets of that
// connection and thus cannot continue to submit them.
log.Debugf("filter: fast-track tracing ICMP/v6: %s", pkt)
_ = pkt.Accept()
return true
}
// Handle echo request and replies regularly.
// Other ICMP packets are considered system business.
icmpLayers := pkt.Layers().LayerClass(layers.LayerClassIPControl)
switch icmpLayer := icmpLayers.(type) {
case *layers.ICMPv4:
switch icmpLayer.TypeCode.Type() {
case layers.ICMPv4TypeEchoRequest,
layers.ICMPv4TypeEchoReply:
return false
}
case *layers.ICMPv6:
switch icmpLayer.TypeCode.Type() {
case layers.ICMPv6TypeEchoRequest,
layers.ICMPv6TypeEchoReply:
return false
}
}
// Permit all ICMP/v6 packets that are not echo requests or replies.
log.Debugf("filter: fast-track accepting ICMP/v6: %s", pkt)
_ = pkt.PermanentAccept()
return true
case packet.UDP, packet.TCP:
switch meta.DstPort {
case 67, 68, 546, 547:
// Always allow DHCP, DHCPv6.
// DHCP and DHCPv6 must be UDP.
if meta.Protocol != packet.UDP {
return false
}
// DHCP is only valid in local network scopes.
switch netutils.ClassifyIP(meta.Dst) { //nolint:exhaustive // Checking for specific values only.
case netutils.HostLocal, netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
default:
return false
}
// Log and permit.
log.Debugf("filter: fast-track accepting DHCP: %s", pkt)
_ = pkt.PermanentAccept()
return true
case apiPort:
// Always allow direct access to the Portmaster API.
// Portmaster API is TCP only.
if meta.Protocol != packet.TCP {
return false
}
// Check if the api port is even set.
if !apiPortSet {
return false
}
// Must be destined for the API IP.
if !meta.Dst.Equal(apiIP) {
return false
}
// Only fast-track local requests.
isMe, err := netenv.IsMyIP(meta.Src)
switch {
case err != nil:
log.Debugf("filter: failed to check if %s is own IP for fast-track: %s", meta.Src, err)
return false
case !isMe:
return false
}
// Log and permit.
log.Debugf("filter: fast-track accepting api connection: %s", pkt)
_ = pkt.PermanentAccept()
return true
case 53:
// Always allow direct access to the Portmaster Nameserver.
// DNS is both UDP and TCP.
// Check if a nameserver IP matcher is set.
if !nameserverIPMatcherReady.IsSet() {
return false
}
// Check if packet is destined for a nameserver IP.
if !nameserverIPMatcher(meta.Dst) {
return false
}
// Only fast-track local requests.
isMe, err := netenv.IsMyIP(meta.Src)
switch {
case err != nil:
log.Debugf("filter: failed to check if %s is own IP for fast-track: %s", meta.Src, err)
return false
case !isMe:
return false
}
// Log and permit.
log.Debugf("filter: fast-track accepting local dns: %s", pkt)
_ = pkt.PermanentAccept()
return true
}
case compat.SystemIntegrationCheckProtocol:
if pkt.Info().Dst.Equal(compat.SystemIntegrationCheckDstIP) {
compat.SubmitSystemIntegrationCheckPacket(pkt)
_ = pkt.Drop()
return true
}
}
return false
}
func initialHandler(conn *network.Connection, pkt packet.Packet) {
log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler")
// Check for special (internal) connection cases.
switch {
case !conn.Inbound && localPortIsPreAuthenticated(conn.Entity.Protocol, conn.LocalPort):
// Approve connection.
conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true
// Redirect outbound DNS packets if enabled,
case dnsQueryInterception() &&
pkt.IsOutbound() &&
pkt.Info().DstPort == 53 &&
// that don't match the address of our nameserver,
nameserverIPMatcherReady.IsSet() &&
!nameserverIPMatcher(pkt.Info().Dst) &&
// and are not broadcast queries by us.
// Context:
// - Unicast queries by the resolver are pre-authenticated.
// - Unicast queries by the compat self-check should be redirected.
!(conn.Process().Pid == ownPID &&
conn.Entity.IPScope == netutils.LocalMulticast):
// Reroute rogue dns queries back to Portmaster.
conn.SetVerdictDirectly(network.VerdictRerouteToNameserver)
conn.Reason.Msg = "redirecting rogue dns query"
conn.Internal = true
// End directly, as no other processing is necessary.
conn.StopFirewallHandler()
finalizeVerdict(conn)
issueVerdict(conn, pkt, 0, true)
return
}
// Apply privacy filter and check tunneling.
filterConnection(pkt.Ctx(), conn, pkt)
// Decide how to continue handling connection.
switch {
case conn.Inspecting:
log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")
conn.SetFirewallHandler(inspectThenVerdict)
inspectThenVerdict(conn, pkt)
default:
conn.StopFirewallHandler()
issueVerdict(conn, pkt, 0, true)
}
}
func filterConnection(ctx context.Context, conn *network.Connection, pkt packet.Packet) {
if filterEnabled() {
log.Tracer(ctx).Trace("filter: starting decision process")
DecideOnConnection(ctx, conn, pkt)
} else {
conn.Accept("privacy filter disabled", noReasonOptionKey)
}
// TODO: Enable inspection framework again.
conn.Inspecting = false
// TODO: Quick fix for the SPN.
// Use inspection framework for proper encryption detection.
switch conn.Entity.DstPort() {
case
22, // SSH
443, // HTTPS
465, // SMTP-SSL
853, // DoT
993, // IMAP-SSL
995: // POP3-SSL
conn.Encrypted = true
}
// Check if connection should be tunneled.
checkTunneling(ctx, conn)
// Handle verdict records and transitions.
finalizeVerdict(conn)
// Request tunneling if no tunnel is set and connection should be tunneled.
if conn.Verdict.Active == network.VerdictRerouteToTunnel &&
conn.TunnelContext == nil {
err := requestTunneling(ctx, conn)
if err != nil {
conn.Failed(fmt.Sprintf("failed to request tunneling: %s", err), "")
finalizeVerdict(conn)
}
}
}
func defaultHandler(conn *network.Connection, pkt packet.Packet) {
// TODO: `pkt` has an active trace log, which we currently don't submit.
issueVerdict(conn, pkt, 0, true)
}
func inspectThenVerdict(conn *network.Connection, pkt packet.Packet) {
pktVerdict, continueInspection := inspection.RunInspectors(conn, pkt)
if continueInspection {
issueVerdict(conn, pkt, pktVerdict, false)
return
}
// we are done with inspecting
conn.StopFirewallHandler()
issueVerdict(conn, pkt, 0, true)
}
func issueVerdict(conn *network.Connection, pkt packet.Packet, verdict network.Verdict, allowPermanent bool) {
// enable permanent verdict
if allowPermanent && !conn.VerdictPermanent {
conn.VerdictPermanent = permanentVerdicts()
if conn.VerdictPermanent {
conn.SaveWhenFinished()
}
}
// do not allow to circumvent decision: e.g. to ACCEPT packets from a DROP-ed connection
if verdict < conn.Verdict.Active {
verdict = conn.Verdict.Active
}
var err error
switch verdict {
case network.VerdictAccept:
atomic.AddUint64(packetsAccepted, 1)
if conn.VerdictPermanent {
err = pkt.PermanentAccept()
} else {
err = pkt.Accept()
}
case network.VerdictBlock:
atomic.AddUint64(packetsBlocked, 1)
if conn.VerdictPermanent {
err = pkt.PermanentBlock()
} else {
err = pkt.Block()
}
case network.VerdictDrop:
atomic.AddUint64(packetsDropped, 1)
if conn.VerdictPermanent {
err = pkt.PermanentDrop()
} else {
err = pkt.Drop()
}
case network.VerdictRerouteToNameserver:
err = pkt.RerouteToNameserver()
case network.VerdictRerouteToTunnel:
err = pkt.RerouteToTunnel()
case network.VerdictFailed:
atomic.AddUint64(packetsFailed, 1)
err = pkt.Drop()
case network.VerdictUndecided, network.VerdictUndeterminable:
log.Warningf("filter: tried to apply verdict %s to pkt %s: dropping instead", verdict, pkt)
fallthrough
default:
atomic.AddUint64(packetsDropped, 1)
err = pkt.Drop()
}
if err != nil {
log.Warningf("filter: failed to apply verdict to pkt %s: %s", pkt, err)
}
}
// verdictRating rates the privacy and security aspect of verdicts from worst to best.
var verdictRating = []network.Verdict{
network.VerdictAccept, // Connection allowed in the open.
network.VerdictRerouteToTunnel, // Connection allowed, but protected.
network.VerdictRerouteToNameserver, // Connection allowed, but resolved via Portmaster.
network.VerdictBlock, // Connection blocked, with feedback.
network.VerdictDrop, // Connection blocked, without feedback.
network.VerdictFailed,
network.VerdictUndeterminable,
network.VerdictUndecided,
}
func finalizeVerdict(conn *network.Connection) {
// Update worst verdict.
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 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) {
// tunnelInfo := GetTunnelInfo(pkt.Info().Dst)
// if tunnelInfo == nil {
// pkt.Block()
// return
// }
//
// entry.CreateTunnel(pkt, tunnelInfo.Domain, tunnelInfo.RRCache.ExportAllARecords())
// log.Tracef("filter: rerouting %s to tunnel entry point", pkt)
// pkt.RerouteToTunnel()
// return
// }
func packetHandler(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case pkt := <-interception.Packets:
interceptionModule.StartWorker("initial packet handler", func(workerCtx context.Context) error {
handlePacket(workerCtx, pkt)
return nil
})
}
}
}
func statLogger(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(10 * time.Second):
log.Tracef(
"filter: packets accepted %d, blocked %d, dropped %d, failed %d",
atomic.LoadUint64(packetsAccepted),
atomic.LoadUint64(packetsBlocked),
atomic.LoadUint64(packetsDropped),
atomic.LoadUint64(packetsFailed),
)
atomic.StoreUint64(packetsAccepted, 0)
atomic.StoreUint64(packetsBlocked, 0)
atomic.StoreUint64(packetsDropped, 0)
atomic.StoreUint64(packetsFailed, 0)
}
}
}