mirror of
https://github.com/safing/portmaster.git
synced 2026-05-20 20:40:36 +00:00
ee8cde31f6
Implement initial proof-of-concept for split tunnel functionality on Windows, allowing applications to route traffic through a designated network interface while bypassing default system routing. Features: - Split tunnel module with TCP/UDP proxy infrastructure - Firewall integration with split tunnel verdict handling - SplitTunneling context attached to connections - Configuration options: enable toggle, interface selection, and policy rules - UI display of split tunnel connection details in connection info panel - Subsystem configuration for user-level access Windows-specific implementation: - Uses proxy-based interface routing on Windows - Automatic or manual interface detection and binding - Support for IPv4 and IPv6 traffic Note: Linux implementation is under development. SPN takes precedence over split tunnel when both are enabled, ensuring SPN connections bypass this feature.
930 lines
31 KiB
Go
930 lines
31 KiB
Go
package network
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/tevino/abool"
|
|
|
|
"github.com/safing/portmaster/base/database/record"
|
|
"github.com/safing/portmaster/base/log"
|
|
"github.com/safing/portmaster/base/notifications"
|
|
"github.com/safing/portmaster/service/intel"
|
|
"github.com/safing/portmaster/service/netenv"
|
|
"github.com/safing/portmaster/service/network/netutils"
|
|
"github.com/safing/portmaster/service/network/packet"
|
|
"github.com/safing/portmaster/service/network/reference"
|
|
"github.com/safing/portmaster/service/process"
|
|
_ "github.com/safing/portmaster/service/process/tags"
|
|
"github.com/safing/portmaster/service/resolver"
|
|
"github.com/safing/portmaster/spn/access"
|
|
"github.com/safing/portmaster/spn/access/account"
|
|
"github.com/safing/portmaster/spn/navigator"
|
|
)
|
|
|
|
// FirewallHandler defines the function signature for a firewall
|
|
// handle function. A firewall handler is responsible for finding
|
|
// a reasonable verdict for the connection conn. The connection is
|
|
// locked before the firewall handler is called.
|
|
type FirewallHandler func(conn *Connection, pkt packet.Packet)
|
|
|
|
// ProcessContext holds additional information about the process
|
|
// that initiated a connection.
|
|
type ProcessContext struct {
|
|
// ProcessName is the name of the process.
|
|
ProcessName string
|
|
// ProfileName is the name of the profile.
|
|
ProfileName string
|
|
// BinaryPath is the path to the process binary.
|
|
BinaryPath string
|
|
// CmdLine holds the execution parameters.
|
|
CmdLine string
|
|
// PID is the process identifier.
|
|
PID int
|
|
// CreatedAt the time when the process was created.
|
|
CreatedAt int64
|
|
// Profile is the ID of the main profile that
|
|
// is applied to the process.
|
|
Profile string
|
|
// Source is the source of the profile.
|
|
Source string
|
|
}
|
|
|
|
// SplitTunContext holds additional information about the split tunnel
|
|
// that a connection is routed through.
|
|
type SplitTunContext struct {
|
|
// Interface is the name of the network interface the connection is bound to.
|
|
Interface string
|
|
// IP is the IP address used to bind the connection to the interface.
|
|
IP net.IP
|
|
}
|
|
|
|
// ConnectionType is a type of connection.
|
|
type ConnectionType int8
|
|
|
|
// Connection Types.
|
|
const (
|
|
Undefined ConnectionType = iota
|
|
IPConnection
|
|
DNSRequest
|
|
)
|
|
|
|
// Connection describes a distinct physical network connection
|
|
// identified by the IP/Port pair.
|
|
type Connection struct { //nolint:maligned // TODO: fix alignment
|
|
record.Base
|
|
sync.Mutex
|
|
|
|
// ID holds a unique request/connection id and is considered immutable after
|
|
// creation.
|
|
ID string
|
|
// Type defines the connection type.
|
|
Type ConnectionType
|
|
// External defines if the connection represents an external request or
|
|
// connection.
|
|
External bool
|
|
// Scope defines the scope of a connection. For DNS requests, the
|
|
// scope is always set to the domain name. For direct packet
|
|
// connections the scope consists of the involved network environment
|
|
// and the packet direction. Once a connection object is created,
|
|
// Scope is considered immutable.
|
|
// Deprecated: This field holds duplicate information, which is accessible
|
|
// clearer through other attributes. Please use conn.Type, conn.Inbound
|
|
// and conn.Entity.Domain instead.
|
|
Scope string
|
|
// IPVersion is set to the packet IP version. It is not set (0) for
|
|
// connections created from a DNS request.
|
|
IPVersion packet.IPVersion
|
|
// Inbound is set to true if the connection is incoming. Inbound is
|
|
// only set when a connection object is created and is considered
|
|
// immutable afterwards.
|
|
Inbound bool
|
|
// IPProtocol is set to the transport protocol used by the connection.
|
|
// Is is considered immutable once a connection object has been
|
|
// created. IPProtocol is not set for connections that have been
|
|
// created from a DNS request.
|
|
IPProtocol packet.IPProtocol
|
|
// LocalIP holds the local IP address of the connection. It is not
|
|
// set for connections created from DNS requests. LocalIP is
|
|
// considered immutable once a connection object has been created.
|
|
LocalIP net.IP
|
|
// LocalIPScope holds the network scope of the local IP.
|
|
LocalIPScope netutils.IPScope
|
|
// LocalPort holds the local port of the connection. It is not
|
|
// set for connections created from DNS requests. LocalPort is
|
|
// considered immutable once a connection object has been created.
|
|
LocalPort uint16
|
|
// PID holds the PID of the owning process.
|
|
PID int
|
|
// Entity describes the remote entity that the connection has been
|
|
// established to. The entity might be changed or information might
|
|
// be added to it during the livetime of a connection. Access to
|
|
// entity must be guarded by the connection lock.
|
|
Entity *intel.Entity
|
|
// Resolver holds information about the resolver used to resolve
|
|
// Entity.Domain.
|
|
Resolver *resolver.ResolverInfo
|
|
// Verdict holds the decisions that are made for a connection
|
|
// The verdict may change so any access to it must be guarded by the
|
|
// connection lock.
|
|
Verdict Verdict
|
|
// Whether or not the connection has been established at least once.
|
|
ConnectionEstablished bool
|
|
// Reason holds information justifying the verdict, as well as additional
|
|
// information about the reason.
|
|
// Access to Reason must be guarded by the connection lock.
|
|
Reason Reason
|
|
// Started holds the number of seconds in UNIX epoch time at which
|
|
// the connection has been initiated and first seen by the portmaster.
|
|
// Started is only ever set when creating a new connection object
|
|
// and is considered immutable afterwards.
|
|
Started int64
|
|
// Ended is set to the number of seconds in UNIX epoch time at which
|
|
// the connection is considered terminated. Ended may be set at any
|
|
// time so access must be guarded by the connection lock.
|
|
Ended int64
|
|
// VerdictPermanent is set to true if the final verdict is permanent
|
|
// and the connection has been (or will be) handed back to the kernel.
|
|
// VerdictPermanent may be changed together with the Verdict and Reason
|
|
// properties and must be guarded using the connection lock.
|
|
VerdictPermanent bool
|
|
// Inspecting is set to true if the connection is being inspected
|
|
// by one or more of the registered inspectors. This property may
|
|
// be changed during the lifetime of a connection and must be guarded
|
|
// using the connection lock.
|
|
Inspecting bool
|
|
// Tunneled is set to true when the connection has been routed through the
|
|
// SPN.
|
|
Tunneled bool
|
|
// Encrypted is currently unused and MUST be ignored.
|
|
Encrypted bool
|
|
// TunnelOpts holds options for tunneling the connection.
|
|
TunnelOpts *navigator.Options
|
|
// ProcessContext holds additional information about the process
|
|
// that initiated the connection. It is set once when the connection
|
|
// object is created and is considered immutable afterwards.
|
|
ProcessContext ProcessContext
|
|
// DNSContext holds additional information about the DNS request that was
|
|
// probably used to resolve the IP of this connection.
|
|
DNSContext *resolver.DNSRequestContext
|
|
// TunnelContext holds additional information about the tunnel that this
|
|
// connection is using.
|
|
TunnelContext interface {
|
|
GetExitNodeID() string
|
|
StopTunnel() error
|
|
}
|
|
// SplitTunContext holds additional information about the split tunnel
|
|
// that this connection is routed through. It is set when the connection
|
|
// verdict is VerdictRerouteToSplitTun and the interface has been resolved.
|
|
SplitTunContext *SplitTunContext
|
|
|
|
// HistoryEnabled is set to true when the connection should be persisted
|
|
// in the history database.
|
|
HistoryEnabled bool
|
|
// BanwidthEnabled is set to true if connection bandwidth data should be persisted
|
|
// in netquery.
|
|
BandwidthEnabled bool
|
|
|
|
// BytesReceived holds the observed received bytes of the connection.
|
|
BytesReceived uint64
|
|
// BytesSent holds the observed sent bytes of the connection.
|
|
BytesSent uint64
|
|
|
|
// lastSeen holds the timestamp when the connection was last seen.
|
|
// If permanent verdicts are enabled and bandwidth reporting is not active,
|
|
// this value will likely not be correct.
|
|
lastSeen atomic.Int64
|
|
|
|
// prompt holds the active prompt for this connection, if there is one.
|
|
prompt *notifications.Notification
|
|
// promptLock locks the prompt separately from the connection.
|
|
// This allows goroutines to dismiss the notification, while another goroutine
|
|
// is waiting for the prompt and holding a lock on the connection.
|
|
promptLock sync.Mutex
|
|
|
|
// pkgQueue is used to serialize packet handling for a single
|
|
// connection and is served by the connections packetHandler.
|
|
pktQueue chan packet.Packet
|
|
// pktQueueActive signifies whether the packet queue is active and may be written to.
|
|
pktQueueActive bool
|
|
// pktQueueLock locks access to pktQueueActive and writing to pktQueue.
|
|
pktQueueLock sync.Mutex
|
|
|
|
// dataComplete signifies that all information about the connection is
|
|
// available and an actual packet has been seen.
|
|
// As long as this flag is not set, the connection may not be evaluated for
|
|
// a verdict and may not be sent to the UI.
|
|
dataComplete *abool.AtomicBool
|
|
// Internal is set to true if the connection is attributed as an
|
|
// Portmaster internal connection. Internal may be set at different
|
|
// points and access to it must be guarded by the connection lock.
|
|
Internal bool
|
|
// process holds a reference to the actor process. That is, the
|
|
// process instance that initiated the connection.
|
|
process *process.Process
|
|
// firewallHandler is the firewall handler that is called for
|
|
// each packet sent to pktQueue.
|
|
firewallHandler FirewallHandler
|
|
// saveWhenFinished can be set to true during the life-time of
|
|
// a connection and signals the firewallHandler that a Save()
|
|
// should be issued after processing the connection.
|
|
saveWhenFinished bool
|
|
// activeInspectors is a slice of booleans where each entry
|
|
// maps to the index of an available inspector. If the value
|
|
// is true the inspector is currently active. False indicates
|
|
// that the inspector has finished and should be skipped.
|
|
activeInspectors []bool
|
|
// inspectorData holds additional meta data for the inspectors.
|
|
// using the inspectors index as a map key.
|
|
inspectorData map[uint8]interface{}
|
|
// ProfileRevisionCounter is used to track changes to the process
|
|
// profile and required for correct re-evaluation of a connections
|
|
// verdict.
|
|
ProfileRevisionCounter uint64
|
|
// addedToMetrics signifies if the connection has already been counted in
|
|
// the metrics.
|
|
addedToMetrics bool
|
|
}
|
|
|
|
// Reason holds information justifying a verdict, as well as additional
|
|
// information about the reason.
|
|
type Reason struct {
|
|
// Msg is a human readable description of the reason.
|
|
Msg string
|
|
// OptionKey is the configuration option key of the setting that
|
|
// was responsible for the verdict.
|
|
OptionKey string
|
|
// Profile is the database key of the profile that held the setting
|
|
// that was responsible for the verdict.
|
|
Profile string
|
|
// ReasonContext may hold additional reason-specific information and
|
|
// any access must be guarded by the connection lock.
|
|
Context interface{}
|
|
}
|
|
|
|
func getProcessContext(ctx context.Context, proc *process.Process) ProcessContext {
|
|
// Gather process information.
|
|
pCtx := ProcessContext{
|
|
ProcessName: proc.Name,
|
|
BinaryPath: proc.Path,
|
|
CmdLine: proc.CmdLine,
|
|
PID: proc.Pid,
|
|
CreatedAt: proc.CreatedAt,
|
|
}
|
|
|
|
// Get local profile.
|
|
localProfile := proc.Profile().LocalProfile()
|
|
if localProfile == nil {
|
|
log.Tracer(ctx).Warningf("network: process %s has no profile", proc)
|
|
return pCtx
|
|
}
|
|
|
|
// Add profile information and return.
|
|
pCtx.ProfileName = localProfile.Name
|
|
pCtx.Profile = localProfile.ID
|
|
pCtx.Source = string(localProfile.Source)
|
|
return pCtx
|
|
}
|
|
|
|
// NewConnectionFromDNSRequest returns a new connection based on the given dns request.
|
|
func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []string, connID string, localIP net.IP, localPort uint16) *Connection {
|
|
// Determine IP version.
|
|
ipVersion := packet.IPv6
|
|
if localIP.To4() != nil {
|
|
ipVersion = packet.IPv4
|
|
}
|
|
|
|
// Create packet info for dns request connection.
|
|
pi := &packet.Info{
|
|
Inbound: false, // outbound as we are looking for the process of the source address
|
|
Version: ipVersion,
|
|
Protocol: packet.UDP,
|
|
Src: localIP, // source as in the process we are looking for
|
|
SrcPort: localPort, // source as in the process we are looking for
|
|
Dst: nil, // do not record direction
|
|
DstPort: 0, // do not record direction
|
|
PID: process.UndefinedProcessID,
|
|
}
|
|
|
|
// Check if the dns request connection was reported with process info.
|
|
var proc *process.Process
|
|
dnsRequestConn, ok := GetDNSRequestConnection(pi)
|
|
switch {
|
|
case !ok:
|
|
// No dns request connection found.
|
|
case dnsRequestConn.PID < 0:
|
|
// Process is not identified or is special.
|
|
case dnsRequestConn.Ended > 0 && dnsRequestConn.Ended < time.Now().Unix()-3:
|
|
// Connection has already ended (too long ago).
|
|
log.Tracer(ctx).Debugf("network: found ended dns request connection %s for dns request for %s", dnsRequestConn, fqdn)
|
|
default:
|
|
log.Tracer(ctx).Debugf("network: found matching dns request connection %s", dnsRequestConn.String())
|
|
// Inherit PID.
|
|
pi.PID = dnsRequestConn.PID
|
|
// Inherit process struct itself, as the PID may already be re-used.
|
|
proc = dnsRequestConn.process
|
|
}
|
|
|
|
// Find process by remote IP/Port.
|
|
if pi.PID == process.UndefinedProcessID {
|
|
pi.PID, _, _ = process.GetPidOfConnection(
|
|
ctx,
|
|
pi,
|
|
)
|
|
}
|
|
|
|
// Get process and profile with PID.
|
|
if proc == nil {
|
|
proc, _ = process.GetProcessWithProfile(ctx, pi.PID)
|
|
}
|
|
|
|
timestamp := time.Now().Unix()
|
|
dnsConn := &Connection{
|
|
ID: connID,
|
|
Type: DNSRequest,
|
|
Scope: fqdn,
|
|
PID: proc.Pid,
|
|
Entity: &intel.Entity{
|
|
Domain: fqdn,
|
|
CNAME: cnames,
|
|
IPScope: netutils.Global, // Assign a global IP scope as default.
|
|
},
|
|
process: proc,
|
|
ProcessContext: getProcessContext(ctx, proc),
|
|
Started: timestamp,
|
|
Ended: timestamp,
|
|
dataComplete: abool.NewBool(true),
|
|
}
|
|
dnsConn.lastSeen.Store(timestamp)
|
|
|
|
// Inherit internal status of profile.
|
|
if localProfile := proc.Profile().LocalProfile(); localProfile != nil {
|
|
dnsConn.Internal = localProfile.Internal
|
|
|
|
if err := dnsConn.UpdateFeatures(); err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
|
|
log.Tracer(ctx).Warningf("network: failed to check for enabled features: %s", err)
|
|
}
|
|
}
|
|
|
|
// DNS Requests are saved by the nameserver depending on the result of the
|
|
// query. Blocked requests are saved immediately, accepted ones are only
|
|
// saved if they are not "used" by a connection.
|
|
|
|
dnsConn.UpdateMeta()
|
|
return dnsConn
|
|
}
|
|
|
|
// NewConnectionFromExternalDNSRequest returns a connection for an external DNS request.
|
|
func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cnames []string, connID string, remoteIP net.IP) (*Connection, error) {
|
|
remoteHost, err := process.GetNetworkHost(ctx, remoteIP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timestamp := time.Now().Unix()
|
|
dnsConn := &Connection{
|
|
ID: connID,
|
|
Type: DNSRequest,
|
|
External: true,
|
|
Scope: fqdn,
|
|
PID: process.NetworkHostProcessID,
|
|
Entity: &intel.Entity{
|
|
Domain: fqdn,
|
|
CNAME: cnames,
|
|
IPScope: netutils.Global, // Assign a global IP scope as default.
|
|
},
|
|
process: remoteHost,
|
|
ProcessContext: getProcessContext(ctx, remoteHost),
|
|
Started: timestamp,
|
|
Ended: timestamp,
|
|
dataComplete: abool.NewBool(true),
|
|
}
|
|
dnsConn.lastSeen.Store(timestamp)
|
|
|
|
// Inherit internal status of profile.
|
|
if localProfile := remoteHost.Profile().LocalProfile(); localProfile != nil {
|
|
dnsConn.Internal = localProfile.Internal
|
|
|
|
if err := dnsConn.UpdateFeatures(); err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
|
|
log.Tracer(ctx).Warningf("network: failed to check for enabled features: %s", err)
|
|
}
|
|
}
|
|
|
|
// DNS Requests are saved by the nameserver depending on the result of the
|
|
// query. Blocked requests are saved immediately, accepted ones are only
|
|
// saved if they are not "used" by a connection.
|
|
|
|
dnsConn.UpdateMeta()
|
|
return dnsConn, nil
|
|
}
|
|
|
|
var tooOldTimestamp = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).Unix()
|
|
|
|
// NewIncompleteConnection creates a new incomplete connection with only minimal information.
|
|
func NewIncompleteConnection(pkt packet.Packet) *Connection {
|
|
info := pkt.Info()
|
|
|
|
// Create new connection object.
|
|
// We do not yet know the direction of the connection for sure, so we can only set minimal information.
|
|
conn := &Connection{
|
|
ID: pkt.GetConnectionID(),
|
|
Type: IPConnection,
|
|
IPVersion: info.Version,
|
|
IPProtocol: info.Protocol,
|
|
Started: info.SeenAt.Unix(),
|
|
PID: info.PID,
|
|
Inbound: info.Inbound,
|
|
dataComplete: abool.NewBool(false),
|
|
}
|
|
conn.lastSeen.Store(conn.Started)
|
|
|
|
// Bullshit check Started timestamp.
|
|
if conn.Started < tooOldTimestamp {
|
|
// Fix timestamp, use current time as fallback.
|
|
conn.Started = time.Now().Unix()
|
|
}
|
|
|
|
// Save connection to internal state in order to mitigate creation of
|
|
// duplicates. Do not propagate yet, as data is not yet complete.
|
|
conn.UpdateMeta()
|
|
conns.add(conn)
|
|
|
|
return conn
|
|
}
|
|
|
|
// GatherConnectionInfo gathers information on the process and remote entity.
|
|
func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
|
|
// Create remote entity.
|
|
if conn.Entity == nil {
|
|
// Remote
|
|
conn.Entity = (&intel.Entity{
|
|
IP: pkt.Info().RemoteIP(),
|
|
Protocol: uint8(pkt.Info().Protocol),
|
|
Port: pkt.Info().RemotePort(),
|
|
}).Init(pkt.Info().DstPort)
|
|
|
|
// Local
|
|
conn.SetLocalIP(pkt.Info().LocalIP())
|
|
conn.LocalPort = pkt.Info().LocalPort()
|
|
|
|
if conn.Inbound {
|
|
switch conn.Entity.IPScope {
|
|
case netutils.HostLocal:
|
|
conn.Scope = IncomingHost
|
|
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
|
|
conn.Scope = IncomingLAN
|
|
case netutils.Global, netutils.GlobalMulticast:
|
|
conn.Scope = IncomingInternet
|
|
|
|
case netutils.Undefined, netutils.Invalid:
|
|
fallthrough
|
|
default:
|
|
conn.Scope = IncomingInvalid
|
|
}
|
|
} else {
|
|
// Outbound direct (possibly P2P) connection.
|
|
switch conn.Entity.IPScope {
|
|
case netutils.HostLocal:
|
|
conn.Scope = PeerHost
|
|
case netutils.LinkLocal, netutils.SiteLocal, netutils.LocalMulticast:
|
|
conn.Scope = PeerLAN
|
|
case netutils.Global, netutils.GlobalMulticast:
|
|
conn.Scope = PeerInternet
|
|
|
|
case netutils.Undefined, netutils.Invalid:
|
|
fallthrough
|
|
default:
|
|
conn.Scope = PeerInvalid
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get PID if not yet available.
|
|
if conn.PID == process.UndefinedProcessID {
|
|
// Get process by looking at the system state tables.
|
|
// Apply direction as reported from the state tables.
|
|
conn.PID, conn.Inbound, _ = process.GetPidOfConnection(pkt.Ctx(), pkt.Info())
|
|
// Errors are informational and are logged to the context.
|
|
}
|
|
|
|
// Only get process and profile with first real packet.
|
|
// TODO: Remove when we got full VM/Docker support.
|
|
if pkt.InfoOnly() {
|
|
return nil
|
|
}
|
|
|
|
// Get Process and Profile.
|
|
if conn.process == nil {
|
|
conn.process, err = process.GetProcessWithProfile(pkt.Ctx(), conn.PID)
|
|
// Errors are informational and are logged to the context.
|
|
if err != nil {
|
|
if pkt.InfoOnly() {
|
|
conn.process = nil // Try again with real packet.
|
|
log.Tracer(pkt.Ctx()).Debugf("network: failed to get process and profile of PID %d: %s", conn.PID, err)
|
|
} else {
|
|
log.Tracer(pkt.Ctx()).Warningf("network: failed to get process and profile of PID %d: %s", conn.PID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply process/profile info to connection.
|
|
if conn.ProfileRevisionCounter == 0 && conn.process != nil {
|
|
// Add process/profile metadata for connection.
|
|
conn.ProcessContext = getProcessContext(pkt.Ctx(), conn.process)
|
|
conn.ProfileRevisionCounter = conn.process.Profile().RevisionCnt()
|
|
|
|
// Inherit internal status of profile.
|
|
if localProfile := conn.process.Profile().LocalProfile(); localProfile != nil {
|
|
conn.Internal = localProfile.Internal
|
|
|
|
if err := conn.UpdateFeatures(); err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
|
|
log.Tracer(pkt.Ctx()).Warningf("network: connection %s failed to check for enabled features: %s", conn, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find domain and DNS context of entity.
|
|
if conn.Entity.Domain == "" && conn.process.Profile() != nil {
|
|
profileScope := conn.process.Profile().LocalProfile().ID
|
|
// check if we can find a domain for that IP
|
|
ipinfo, err := resolver.GetIPInfo(profileScope, pkt.Info().RemoteIP().String())
|
|
if err != nil {
|
|
// Try again with the global scope, in case DNS went through the system resolver.
|
|
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
|
|
}
|
|
|
|
if runtime.GOOS == "windows" && err != nil {
|
|
// On windows domains may come with delay.
|
|
if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
|
|
// Flush the dns listener buffer and try again.
|
|
for i := range 4 {
|
|
err = module.instance.DNSMonitor().Flush()
|
|
if err != nil {
|
|
// Error flushing, dont try again.
|
|
break
|
|
}
|
|
// Try with profile scope
|
|
ipinfo, err = resolver.GetIPInfo(profileScope, pkt.Info().RemoteIP().String())
|
|
if err == nil {
|
|
log.Tracer(pkt.Ctx()).Debugf("network: found domain with scope (%s) from dnsmonitor after %d tries", profileScope, +1)
|
|
break
|
|
}
|
|
// Try again with the global scope
|
|
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
|
|
if err == nil {
|
|
log.Tracer(pkt.Ctx()).Debugf("network: found domain from dnsmonitor after %d tries", i+1)
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
lastResolvedDomain := ipinfo.MostRecentDomain()
|
|
if lastResolvedDomain != nil {
|
|
conn.Scope = lastResolvedDomain.Domain
|
|
conn.Entity.Domain = lastResolvedDomain.Domain
|
|
conn.Entity.CNAME = lastResolvedDomain.CNAMEs
|
|
conn.DNSContext = lastResolvedDomain.DNSRequestContext
|
|
conn.Resolver = lastResolvedDomain.Resolver
|
|
removeOpenDNSRequest(conn.process.Pid, lastResolvedDomain.Domain)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if destination IP is the captive portal's IP.
|
|
if conn.Entity.Domain == "" {
|
|
portal := netenv.GetCaptivePortal()
|
|
if pkt.Info().RemoteIP().Equal(portal.IP) {
|
|
conn.Scope = portal.Domain
|
|
conn.Entity.Domain = portal.Domain
|
|
}
|
|
}
|
|
|
|
// Check if we have all required data for a complete packet.
|
|
switch {
|
|
case pkt.InfoOnly():
|
|
// We need a full packet.
|
|
case conn.process == nil:
|
|
// We need a process.
|
|
case conn.process.Profile() == nil:
|
|
// We need a profile.
|
|
case conn.Entity == nil:
|
|
// We need an entity.
|
|
default:
|
|
// Data is complete!
|
|
conn.dataComplete.Set()
|
|
}
|
|
|
|
conn.SaveWhenFinished()
|
|
return nil
|
|
}
|
|
|
|
// GetConnection fetches a Connection from the database.
|
|
func GetConnection(connID string) (*Connection, bool) {
|
|
return conns.get(connID)
|
|
}
|
|
|
|
// GetAllConnections Gets all connection.
|
|
func GetAllConnections() []*Connection {
|
|
return conns.list()
|
|
}
|
|
|
|
// GetDNSConnection fetches a DNS Connection from the database.
|
|
func GetDNSConnection(dnsConnID string) (*Connection, bool) {
|
|
return dnsConns.get(dnsConnID)
|
|
}
|
|
|
|
// SetLocalIP sets the local IP address together with its network scope. The
|
|
// connection is not locked for this.
|
|
func (conn *Connection) SetLocalIP(ip net.IP) {
|
|
conn.LocalIP = ip
|
|
conn.LocalIPScope = netutils.GetIPScope(ip)
|
|
}
|
|
|
|
// UpdateFeatures checks which connection related features may and should be
|
|
// used and sets the flags accordingly.
|
|
// The caller must hold a lock on the connection.
|
|
func (conn *Connection) UpdateFeatures() error {
|
|
// Get user.
|
|
user, err := access.GetUser()
|
|
if err != nil && !errors.Is(err, access.ErrNotLoggedIn) {
|
|
return err
|
|
}
|
|
// Caution: user may be nil!
|
|
|
|
// Check if history may be used and if it is enabled for this application.
|
|
conn.HistoryEnabled = false
|
|
switch {
|
|
case conn.Internal:
|
|
// Do not record internal connections, as they are of low interest in the history.
|
|
// TODO: Should we create a setting for this?
|
|
case conn.Entity.IPScope.IsLocalhost():
|
|
// Do not record localhost-only connections, as they are very low interest in the history.
|
|
// TODO: Should we create a setting for this?
|
|
case user.MayUse(account.FeatureHistory):
|
|
// Check if history may be used and is enabled.
|
|
lProfile := conn.Process().Profile()
|
|
if lProfile != nil {
|
|
conn.HistoryEnabled = lProfile.EnableHistory()
|
|
}
|
|
}
|
|
|
|
// Check if bandwidth visibility may be used.
|
|
conn.BandwidthEnabled = user.MayUse(account.FeatureBWVis)
|
|
|
|
return nil
|
|
}
|
|
|
|
// AcceptWithContext accepts the connection.
|
|
func (conn *Connection) AcceptWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
|
if !conn.SetVerdict(VerdictAccept, reason, reasonOptionKey, ctx) {
|
|
log.Warningf("filter: tried to accept %s, but current verdict is %s", conn, conn.Verdict)
|
|
}
|
|
}
|
|
|
|
// Accept is like AcceptWithContext but only accepts a reason.
|
|
func (conn *Connection) Accept(reason, reasonOptionKey string) {
|
|
conn.AcceptWithContext(reason, reasonOptionKey, nil)
|
|
}
|
|
|
|
// BlockWithContext blocks the connection.
|
|
func (conn *Connection) BlockWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
|
if !conn.SetVerdict(VerdictBlock, reason, reasonOptionKey, ctx) {
|
|
log.Warningf("filter: tried to block %s, but current verdict is %s", conn, conn.Verdict)
|
|
}
|
|
}
|
|
|
|
// Block is like BlockWithContext but does only accepts a reason.
|
|
func (conn *Connection) Block(reason, reasonOptionKey string) {
|
|
conn.BlockWithContext(reason, reasonOptionKey, nil)
|
|
}
|
|
|
|
// DropWithContext drops the connection.
|
|
func (conn *Connection) DropWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
|
if !conn.SetVerdict(VerdictDrop, reason, reasonOptionKey, ctx) {
|
|
log.Warningf("filter: tried to drop %s, but current verdict is %s", conn, conn.Verdict)
|
|
}
|
|
}
|
|
|
|
// Drop is like DropWithContext but does only accepts a reason.
|
|
func (conn *Connection) Drop(reason, reasonOptionKey string) {
|
|
conn.DropWithContext(reason, reasonOptionKey, nil)
|
|
}
|
|
|
|
// DenyWithContext blocks or drops the link depending on the connection direction.
|
|
func (conn *Connection) DenyWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
|
if conn.Inbound {
|
|
conn.DropWithContext(reason, reasonOptionKey, ctx)
|
|
} else {
|
|
conn.BlockWithContext(reason, reasonOptionKey, ctx)
|
|
}
|
|
}
|
|
|
|
// Deny is like DenyWithContext but only accepts a reason.
|
|
func (conn *Connection) Deny(reason, reasonOptionKey string) {
|
|
conn.DenyWithContext(reason, reasonOptionKey, nil)
|
|
}
|
|
|
|
// FailedWithContext marks the connection with VerdictFailed and stores the reason.
|
|
func (conn *Connection) FailedWithContext(reason, reasonOptionKey string, ctx interface{}) {
|
|
if !conn.SetVerdict(VerdictFailed, reason, reasonOptionKey, ctx) {
|
|
log.Warningf("filter: tried to drop %s due to error but current verdict is %s", conn, conn.Verdict)
|
|
}
|
|
}
|
|
|
|
// Failed is like FailedWithContext but only accepts a string.
|
|
func (conn *Connection) Failed(reason, reasonOptionKey string) {
|
|
conn.FailedWithContext(reason, reasonOptionKey, nil)
|
|
}
|
|
|
|
// SetVerdict sets a new verdict for the connection.
|
|
func (conn *Connection) SetVerdict(newVerdict Verdict, reason, reasonOptionKey string, reasonCtx interface{}) (ok bool) {
|
|
conn.SetVerdictDirectly(newVerdict)
|
|
|
|
// Set reason and context.
|
|
conn.Reason.Msg = reason
|
|
conn.Reason.Context = reasonCtx
|
|
|
|
// Reset option key.
|
|
conn.Reason.OptionKey = ""
|
|
conn.Reason.Profile = ""
|
|
|
|
// Set option key if data is available.
|
|
if reasonOptionKey != "" {
|
|
lp := conn.Process().Profile()
|
|
if lp != nil {
|
|
conn.Reason.OptionKey = reasonOptionKey
|
|
conn.Reason.Profile = lp.GetProfileSource(conn.Reason.OptionKey)
|
|
}
|
|
}
|
|
|
|
return true // TODO: remove
|
|
}
|
|
|
|
// SetVerdictDirectly sets the verdict.
|
|
func (conn *Connection) SetVerdictDirectly(newVerdict Verdict) {
|
|
conn.Verdict = newVerdict
|
|
}
|
|
|
|
// VerdictVerb returns the verdict as a verb, while taking any special states
|
|
// into account.
|
|
func (conn *Connection) VerdictVerb() string {
|
|
return conn.Verdict.Verb()
|
|
}
|
|
|
|
// DataIsComplete returns whether all information about the connection is
|
|
// available and an actual packet has been seen.
|
|
// As long as this flag is not set, the connection may not be evaluated for
|
|
// a verdict and may not be sent to the UI.
|
|
func (conn *Connection) DataIsComplete() bool {
|
|
return conn.dataComplete.IsSet()
|
|
}
|
|
|
|
// Process returns the connection's process.
|
|
func (conn *Connection) Process() *process.Process {
|
|
return conn.process
|
|
}
|
|
|
|
// SaveWhenFinished marks the connection for saving it after the firewall handler.
|
|
func (conn *Connection) SaveWhenFinished() {
|
|
conn.saveWhenFinished = true
|
|
}
|
|
|
|
// Save saves the connection in the storage and propagates the change
|
|
// through the database system. Save may lock dnsConnsLock or connsLock
|
|
// in if Save() is called the first time.
|
|
// Callers must make sure to lock the connection itself before calling
|
|
// Save().
|
|
func (conn *Connection) Save() {
|
|
conn.UpdateMeta()
|
|
|
|
// nolint:exhaustive
|
|
switch conn.Verdict {
|
|
case VerdictAccept, VerdictRerouteToNameserver, VerdictRerouteToSplitTun:
|
|
conn.ConnectionEstablished = true
|
|
case VerdictRerouteToTunnel:
|
|
// this is already handled when the connection tunnel has been
|
|
// established.
|
|
default:
|
|
}
|
|
|
|
// Do not save/update until data is complete.
|
|
if !conn.DataIsComplete() {
|
|
return
|
|
}
|
|
|
|
if !conn.KeyIsSet() {
|
|
if conn.Type == DNSRequest {
|
|
conn.SetKey(makeKey(conn.process.Pid, dbScopeDNS, conn.ID))
|
|
dnsConns.add(conn)
|
|
} else {
|
|
conn.SetKey(makeKey(conn.process.Pid, dbScopeIP, conn.ID))
|
|
conns.add(conn)
|
|
}
|
|
}
|
|
|
|
conn.addToMetrics()
|
|
|
|
// notify database controller
|
|
dbController.PushUpdate(conn)
|
|
}
|
|
|
|
// delete deletes a link from the storage and propagates the change.
|
|
// delete may lock either the dnsConnsLock or connsLock. Callers
|
|
// must still make sure to lock the connection itself.
|
|
func (conn *Connection) delete() {
|
|
// A connection without an ID has been created from
|
|
// a DNS request rather than a packet. Choose the correct
|
|
// connection store here.
|
|
if conn.Type == IPConnection {
|
|
conns.delete(conn)
|
|
} else {
|
|
dnsConns.delete(conn)
|
|
}
|
|
|
|
conn.Meta().Delete()
|
|
|
|
// Notify database controller if data is complete and thus connection was previously exposed.
|
|
if conn.DataIsComplete() {
|
|
dbController.PushUpdate(conn)
|
|
}
|
|
}
|
|
|
|
// GetActiveInspectors returns the list of active inspectors.
|
|
func (conn *Connection) GetActiveInspectors() []bool {
|
|
return conn.activeInspectors
|
|
}
|
|
|
|
// SetActiveInspectors sets the list of active inspectors.
|
|
func (conn *Connection) SetActiveInspectors(newInspectors []bool) {
|
|
conn.activeInspectors = newInspectors
|
|
}
|
|
|
|
// GetInspectorData returns the list of inspector data.
|
|
func (conn *Connection) GetInspectorData() map[uint8]interface{} {
|
|
return conn.inspectorData
|
|
}
|
|
|
|
// SetInspectorData set the list of inspector data.
|
|
func (conn *Connection) SetInspectorData(newInspectorData map[uint8]interface{}) {
|
|
conn.inspectorData = newInspectorData
|
|
}
|
|
|
|
// SetPrompt sets the given prompt on the connection.
|
|
// If there already is a prompt set, the previous prompt notification is deleted.
|
|
func (conn *Connection) SetPrompt(prompt *notifications.Notification) {
|
|
conn.promptLock.Lock()
|
|
defer conn.promptLock.Unlock()
|
|
|
|
if conn.prompt != nil {
|
|
conn.prompt.Delete()
|
|
}
|
|
conn.prompt = prompt
|
|
}
|
|
|
|
// RemovePrompt removes the prompt on the connection.
|
|
func (conn *Connection) RemovePrompt() {
|
|
conn.promptLock.Lock()
|
|
defer conn.promptLock.Unlock()
|
|
|
|
if conn.prompt != nil {
|
|
conn.prompt.Delete()
|
|
}
|
|
}
|
|
|
|
// String returns a string representation of conn.
|
|
func (conn *Connection) String() string {
|
|
switch {
|
|
case conn.process == nil || conn.Entity == nil:
|
|
return conn.ID
|
|
case conn.Inbound:
|
|
return fmt.Sprintf("%s <- %s", conn.process, conn.Entity.IP)
|
|
case conn.Entity.Domain != "":
|
|
return fmt.Sprintf("%s to %s (%s)", conn.process, conn.Entity.Domain, conn.Entity.IP)
|
|
default:
|
|
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
|
|
}
|
|
}
|
|
|
|
func (conn *Connection) shouldWaitForDomain() bool {
|
|
// Should wait for Global Unicast, outgoing and not ICMP connections
|
|
switch {
|
|
case conn.Entity.IPScope != netutils.Global:
|
|
return false
|
|
case conn.Inbound:
|
|
return false
|
|
case reference.IsICMP(conn.Entity.Protocol):
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|