wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

View File

@@ -0,0 +1,102 @@
package state
import (
"time"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/socket"
)
const (
// UDPConnectionTTL defines the duration after which unseen UDP connections are regarded as ended.
UDPConnectionTTL = 10 * time.Minute
)
// Exists checks if the given connection is present in the system state tables.
func Exists(pktInfo *packet.Info, now time.Time) (exists bool) {
// TODO: create lookup maps before running a flurry of Exists() checks.
switch {
case pktInfo.Version == packet.IPv4 && pktInfo.Protocol == packet.TCP:
return tcp4Table.exists(pktInfo)
case pktInfo.Version == packet.IPv6 && pktInfo.Protocol == packet.TCP:
return tcp6Table.exists(pktInfo)
case pktInfo.Version == packet.IPv4 && pktInfo.Protocol == packet.UDP:
return udp4Table.exists(pktInfo, now)
case pktInfo.Version == packet.IPv6 && pktInfo.Protocol == packet.UDP:
return udp6Table.exists(pktInfo, now)
default:
return false
}
}
func (table *tcpTable) exists(pktInfo *packet.Info) (exists bool) {
// Update tables if older than the connection that is checked.
if table.lastUpdateAt.Load() < pktInfo.SeenAt.UnixNano() {
table.updateTables()
}
table.lock.RLock()
defer table.lock.RUnlock()
localIP := pktInfo.LocalIP()
localPort := pktInfo.LocalPort()
remoteIP := pktInfo.RemoteIP()
remotePort := pktInfo.RemotePort()
// search connections
for _, socketInfo := range table.connections {
if localPort == socketInfo.Local.Port &&
remotePort == socketInfo.Remote.Port &&
remoteIP.Equal(socketInfo.Remote.IP) &&
localIP.Equal(socketInfo.Local.IP) {
return true
}
}
return false
}
func (table *udpTable) exists(pktInfo *packet.Info, now time.Time) (exists bool) {
// Update tables if older than the connection that is checked.
if table.lastUpdateAt.Load() < pktInfo.SeenAt.UnixNano() {
table.updateTables()
}
table.lock.RLock()
defer table.lock.RUnlock()
localIP := pktInfo.LocalIP()
localPort := pktInfo.LocalPort()
remoteIP := pktInfo.RemoteIP()
remotePort := pktInfo.RemotePort()
connThreshhold := now.Add(-UDPConnectionTTL)
// search binds
for _, socketInfo := range table.binds {
if localPort == socketInfo.Local.Port &&
(socketInfo.Local.IP[0] == 0 || localIP.Equal(socketInfo.Local.IP)) {
udpConnState, ok := table.getConnState(socketInfo, socket.Address{
IP: remoteIP,
Port: remotePort,
})
switch {
case !ok:
return false
case udpConnState.lastSeen.After(connThreshhold):
return true
default:
return false
}
}
}
return false
}

View File

@@ -0,0 +1,38 @@
package state
import (
"sync"
"github.com/safing/portbase/database/record"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/socket"
)
// Info holds network state information as provided by the system.
type Info struct {
record.Base
sync.Mutex
TCP4Connections []*socket.ConnectionInfo
TCP4Listeners []*socket.BindInfo
TCP6Connections []*socket.ConnectionInfo
TCP6Listeners []*socket.BindInfo
UDP4Binds []*socket.BindInfo
UDP6Binds []*socket.BindInfo
}
// GetInfo returns all system state tables. The returned data must not be modified.
func GetInfo() *Info {
info := &Info{}
info.TCP4Connections, info.TCP4Listeners = tcp4Table.updateTables()
info.UDP4Binds = udp4Table.updateTables()
if netenv.IPv6Enabled() {
info.TCP6Connections, info.TCP6Listeners = tcp6Table.updateTables()
info.UDP6Binds = udp6Table.updateTables()
}
info.UpdateMeta()
return info
}

View File

@@ -0,0 +1,264 @@
package state
import (
"errors"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/socket"
)
// - TCP
// - Outbound: Match listeners (in!), then connections (out!)
// - Inbound: Match listeners (in!), then connections (out!)
// - Clean via connections
// - UDP
// - Any connection: match specific local address or zero IP
// - In or out: save direction of first packet:
// - map[<local udp bind ip+port>]map[<remote ip+port>]{direction, lastSeen}
// - only clean if <local udp bind ip+port> is removed by OS
// - limit <remote ip+port> to 256 entries?
// - clean <remote ip+port> after 72hrs?
// - switch direction to outbound if outbound packet is seen?
// - IP: Unidentified Process
// Errors.
var (
ErrConnectionNotFound = errors.New("could not find connection in system state tables")
ErrPIDNotFound = errors.New("could not find pid for socket inode")
)
const (
lookupTries = 5
fastLookupTries = 2
)
// Lookup looks for the given connection in the system state tables and returns the PID of the associated process and whether the connection is inbound.
func Lookup(pktInfo *packet.Info, fast bool) (pid int, inbound bool, err error) {
// auto-detect version
if pktInfo.Version == 0 {
if ip := pktInfo.LocalIP().To4(); ip != nil {
pktInfo.Version = packet.IPv4
} else {
pktInfo.Version = packet.IPv6
}
}
switch {
case pktInfo.Version == packet.IPv4 && pktInfo.Protocol == packet.TCP:
return tcp4Table.lookup(pktInfo, fast)
case pktInfo.Version == packet.IPv6 && pktInfo.Protocol == packet.TCP:
return tcp6Table.lookup(pktInfo, fast)
case pktInfo.Version == packet.IPv4 && pktInfo.Protocol == packet.UDP:
return udp4Table.lookup(pktInfo, fast)
case pktInfo.Version == packet.IPv6 && pktInfo.Protocol == packet.UDP:
return udp6Table.lookup(pktInfo, fast)
default:
return socket.UndefinedProcessID, pktInfo.Inbound, errors.New("unsupported protocol for finding process")
}
}
func (table *tcpTable) lookup(pktInfo *packet.Info, fast bool) (
pid int,
inbound bool,
err error,
) {
// Prepare variables.
var (
connections []*socket.ConnectionInfo
listeners []*socket.BindInfo
dualStackConnections []*socket.ConnectionInfo
dualStackListeners []*socket.BindInfo
)
// Search for the socket until found.
for i := 1; i <= lookupTries; i++ {
// Use existing tables for first check if packet was seen after last table update.
if i == 1 && pktInfo.SeenAt.UnixNano() >= table.lastUpdateAt.Load() {
connections, listeners = table.getCurrentTables()
} else {
connections, listeners = table.updateTables()
}
// Check tables for socket.
socketInfo, inbound := findTCPSocket(pktInfo, connections, listeners)
// If there's a match, check if we have the PID and return.
if socketInfo != nil {
return CheckPID(socketInfo, inbound)
}
// DUAL-STACK
// Skip if dualStack is not enabled.
if table.dualStack == nil {
continue
}
// Use existing tables for first check if packet was seen after last table update.
if i == 1 && pktInfo.SeenAt.UnixNano() >= table.dualStack.lastUpdateAt.Load() {
dualStackConnections, dualStackListeners = table.dualStack.getCurrentTables()
} else {
dualStackConnections, dualStackListeners = table.dualStack.updateTables()
}
// Check tables for socket.
socketInfo, inbound = findTCPSocket(pktInfo, dualStackConnections, dualStackListeners)
// If there's a match, check if we have the PID and return.
if socketInfo != nil {
return CheckPID(socketInfo, inbound)
}
// Search less if we want to be fast.
if fast && i >= fastLookupTries {
break
}
}
return socket.UndefinedProcessID, pktInfo.Inbound, ErrConnectionNotFound
}
func findTCPSocket(
pktInfo *packet.Info,
connections []*socket.ConnectionInfo,
listeners []*socket.BindInfo,
) (
socketInfo socket.Info,
inbound bool,
) {
localIP := pktInfo.LocalIP()
localPort := pktInfo.LocalPort()
// always search listeners first
for _, socketInfo := range listeners {
if localPort == socketInfo.Local.Port &&
(socketInfo.ListensAny || localIP.Equal(socketInfo.Local.IP)) {
return socketInfo, true
}
}
remoteIP := pktInfo.RemoteIP()
remotePort := pktInfo.RemotePort()
// search connections
for _, socketInfo := range connections {
if localPort == socketInfo.Local.Port &&
remotePort == socketInfo.Remote.Port &&
remoteIP.Equal(socketInfo.Remote.IP) &&
localIP.Equal(socketInfo.Local.IP) {
return socketInfo, false
}
}
return nil, false
}
func (table *udpTable) lookup(pktInfo *packet.Info, fast bool) (
pid int,
inbound bool,
err error,
) {
// TODO: Currently broadcast/multicast scopes are not checked, so we might
// attribute an incoming broadcast/multicast packet to the wrong process if
// there are multiple processes listening on the same local port, but
// binding to different addresses. This highly unusual for clients.
isInboundMulticast := pktInfo.Inbound && netutils.GetIPScope(pktInfo.LocalIP()) == netutils.LocalMulticast
// Prepare variables.
var (
binds []*socket.BindInfo
dualStackBinds []*socket.BindInfo
)
// Search for the socket until found.
for i := 1; i <= lookupTries; i++ {
// Get or update tables.
if i == 1 && pktInfo.SeenAt.UnixNano() >= table.lastUpdateAt.Load() {
binds = table.getCurrentTables()
} else {
binds = table.updateTables()
}
// Check tables for socket.
socketInfo := findUDPSocket(pktInfo, binds, isInboundMulticast)
// If there's a match, do some last checks and return.
if socketInfo != nil {
// If there is no remote port, do check for the direction of the
// connection. This will be the case for pure checking functions
// that do not want to change direction state.
if pktInfo.RemotePort() == 0 {
return CheckPID(socketInfo, pktInfo.Inbound)
}
// Get (and save) the direction of the connection.
connInbound := table.getDirection(socketInfo, pktInfo)
// Check we have the PID and return.
return CheckPID(socketInfo, connInbound)
}
// DUAL-STACK
// Skip if dualStack is not enabled.
if table.dualStack == nil {
continue
}
// Get or update tables.
if i == 1 && pktInfo.SeenAt.UnixNano() >= table.lastUpdateAt.Load() {
dualStackBinds = table.dualStack.getCurrentTables()
} else {
dualStackBinds = table.dualStack.updateTables()
}
// Check tables for socket.
socketInfo = findUDPSocket(pktInfo, dualStackBinds, isInboundMulticast)
// If there's a match, do some last checks and return.
if socketInfo != nil {
// If there is no remote port, do check for the direction of the
// connection. This will be the case for pure checking functions
// that do not want to change direction state.
if pktInfo.RemotePort() == 0 {
return CheckPID(socketInfo, pktInfo.Inbound)
}
// Get (and save) the direction of the connection.
connInbound := table.getDirection(socketInfo, pktInfo)
// Check we have the PID and return.
return CheckPID(socketInfo, connInbound)
}
// Search less if we want to be fast.
if fast && i >= fastLookupTries {
break
}
}
return socket.UndefinedProcessID, pktInfo.Inbound, ErrConnectionNotFound
}
func findUDPSocket(pktInfo *packet.Info, binds []*socket.BindInfo, isInboundMulticast bool) (socketInfo *socket.BindInfo) {
localIP := pktInfo.LocalIP()
localPort := pktInfo.LocalPort()
// search binds
for _, socketInfo := range binds {
if localPort == socketInfo.Local.Port &&
(socketInfo.ListensAny || // zero IP (dual-stack)
isInboundMulticast || // inbound broadcast, multicast
localIP.Equal(socketInfo.Local.IP)) {
return socketInfo
}
}
return nil
}

View File

@@ -0,0 +1,46 @@
//go:build !windows && !linux
// +build !windows,!linux
package state
import (
"time"
"github.com/safing/portbase/config"
"github.com/safing/portmaster/service/network/socket"
)
func init() {
// This increases performance on unsupported system.
// It's not critical at all and does not break anything if it fails.
go func() {
// Wait for one minute before we set the default value, as we
// currently cannot easily integrate into the startup procedure.
time.Sleep(1 * time.Minute)
// We cannot use process.CfgOptionEnableProcessDetectionKey, because of an import loop.
config.SetDefaultConfigOption("core/enableProcessDetection", false)
}()
}
func getTCP4Table() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error) {
return nil, nil, nil
}
func getTCP6Table() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error) {
return nil, nil, nil
}
func getUDP4Table() (binds []*socket.BindInfo, err error) {
return nil, nil
}
func getUDP6Table() (binds []*socket.BindInfo, err error) {
return nil, nil
}
// CheckPID checks the if socket info already has a PID and if not, tries to find it.
// Depending on the OS, this might be a no-op.
func CheckPID(socketInfo socket.Info, connInbound bool) (pid int, inbound bool, err error) {
return socketInfo.GetPID(), connInbound, nil
}

View File

@@ -0,0 +1,40 @@
package state
import (
"time"
"github.com/safing/portmaster/service/network/proc"
"github.com/safing/portmaster/service/network/socket"
)
var (
getTCP4Table = proc.GetTCP4Table
getTCP6Table = proc.GetTCP6Table
getUDP4Table = proc.GetUDP4Table
getUDP6Table = proc.GetUDP6Table
checkPIDTries = 5
checkPIDBaseWaitTime = 5 * time.Millisecond
)
// CheckPID checks the if socket info already has a PID and if not, tries to find it.
// Depending on the OS, this might be a no-op.
func CheckPID(socketInfo socket.Info, connInbound bool) (pid int, inbound bool, err error) {
for i := 1; i <= checkPIDTries; i++ {
// look for PID
pid = proc.GetPID(socketInfo)
if pid != socket.UndefinedProcessID {
// if we found a PID, return
break
}
// every time, except for the last iteration
if i < checkPIDTries {
// we found no PID, we could have been too fast, give the kernel some time to think
// back off timer: with 5ms baseWaitTime: 5, 10, 15, 20, 25 - 75ms in total
time.Sleep(time.Duration(i) * checkPIDBaseWaitTime)
}
}
return pid, connInbound, nil
}

View File

@@ -0,0 +1,19 @@
package state
import (
"github.com/safing/portmaster/service/network/iphelper"
"github.com/safing/portmaster/service/network/socket"
)
var (
getTCP4Table = iphelper.GetTCP4Table
getTCP6Table = iphelper.GetTCP6Table
getUDP4Table = iphelper.GetUDP4Table
getUDP6Table = iphelper.GetUDP6Table
)
// CheckPID checks the if socket info already has a PID and if not, tries to find it.
// Depending on the OS, this might be a no-op.
func CheckPID(socketInfo socket.Info, connInbound bool) (pid int, inbound bool, err error) {
return socketInfo.GetPID(), connInbound, nil
}

View File

@@ -0,0 +1,91 @@
package state
import (
"net"
"sync"
"sync/atomic"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/service/network/socket"
)
const (
minDurationBetweenTableUpdates = 10 * time.Millisecond
)
type tcpTable struct {
version int
connections []*socket.ConnectionInfo
listeners []*socket.BindInfo
lock sync.RWMutex
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
lastUpdateAt atomic.Int64
fetchLimiter *utils.CallLimiter
fetchTable func() (connections []*socket.ConnectionInfo, listeners []*socket.BindInfo, err error)
dualStack *tcpTable
}
var (
tcp6Table = &tcpTable{
version: 6,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchTable: getTCP6Table,
}
tcp4Table = &tcpTable{
version: 4,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchTable: getTCP4Table,
}
)
// EnableTCPDualStack adds the TCP6 table to the TCP4 table as a dual-stack.
// Must be called before any lookup operation.
func EnableTCPDualStack() {
tcp4Table.dualStack = tcp6Table
}
func (table *tcpTable) getCurrentTables() (
connections []*socket.ConnectionInfo,
listeners []*socket.BindInfo,
) {
table.lock.RLock()
defer table.lock.RUnlock()
return table.connections, table.listeners
}
func (table *tcpTable) updateTables() (
connections []*socket.ConnectionInfo,
listeners []*socket.BindInfo,
) {
// Fetch tables.
table.fetchLimiter.Do(func() {
// Fetch new tables from system.
connections, listeners, err := table.fetchTable()
if err != nil {
log.Warningf("state: failed to get TCP%d socket table: %s", table.version, err)
return
}
// Pre-check for any listeners.
for _, bindInfo := range listeners {
bindInfo.ListensAny = bindInfo.Local.IP.Equal(net.IPv4zero) || bindInfo.Local.IP.Equal(net.IPv6zero)
}
// Apply new tables.
table.lock.Lock()
defer table.lock.Unlock()
table.connections = connections
table.listeners = listeners
table.lastUpdateAt.Store(time.Now().UnixNano())
})
return table.getCurrentTables()
}

View File

@@ -0,0 +1,210 @@
package state
import (
"context"
"net"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/network/socket"
)
type udpTable struct {
version int
binds []*socket.BindInfo
lock sync.RWMutex
// lastUpdateAt stores the time when the tables where last updated as unix nanoseconds.
lastUpdateAt atomic.Int64
fetchLimiter *utils.CallLimiter
fetchTable func() (binds []*socket.BindInfo, err error)
states map[string]map[string]*udpState
statesLock sync.Mutex
dualStack *udpTable
}
type udpState struct {
inbound bool
lastSeen time.Time
}
const (
// UDPConnStateTTL is the maximum time a udp connection state is held.
UDPConnStateTTL = 72 * time.Hour
// UDPConnStateShortenedTTL is a shortened maximum time a udp connection state is held, if there more entries than defined by AggressiveCleaningThreshold.
UDPConnStateShortenedTTL = 3 * time.Hour
// AggressiveCleaningThreshold defines the soft limit of udp connection state held per udp socket.
AggressiveCleaningThreshold = 256
)
var (
udp6Table = &udpTable{
version: 6,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchTable: getUDP6Table,
states: make(map[string]map[string]*udpState),
}
udp4Table = &udpTable{
version: 4,
fetchLimiter: utils.NewCallLimiter(minDurationBetweenTableUpdates),
fetchTable: getUDP4Table,
states: make(map[string]map[string]*udpState),
}
)
// EnableUDPDualStack adds the UDP6 table to the UDP4 table as a dual-stack.
// Must be called before any lookup operation.
func EnableUDPDualStack() {
udp4Table.dualStack = udp6Table
}
func (table *udpTable) getCurrentTables() (binds []*socket.BindInfo) {
table.lock.RLock()
defer table.lock.RUnlock()
return table.binds
}
func (table *udpTable) updateTables() (binds []*socket.BindInfo) {
// Fetch tables.
table.fetchLimiter.Do(func() {
// Fetch new tables from system.
binds, err := table.fetchTable()
if err != nil {
log.Warningf("state: failed to get UDP%d socket table: %s", table.version, err)
return
}
// Pre-check for any listeners.
for _, bindInfo := range binds {
bindInfo.ListensAny = bindInfo.Local.IP.Equal(net.IPv4zero) || bindInfo.Local.IP.Equal(net.IPv6zero)
}
// Apply new tables.
table.lock.Lock()
defer table.lock.Unlock()
table.binds = binds
table.lastUpdateAt.Store(time.Now().UnixNano())
})
return table.getCurrentTables()
}
// CleanUDPStates cleans the udp connection states which save connection directions.
func CleanUDPStates(_ context.Context) {
now := time.Now().UTC()
udp4Table.updateTables()
udp4Table.cleanStates(now)
if netenv.IPv6Enabled() {
udp6Table.updateTables()
udp6Table.cleanStates(now)
}
}
func (table *udpTable) getConnState(
socketInfo *socket.BindInfo,
remoteAddress socket.Address,
) (udpConnState *udpState, ok bool) {
table.statesLock.Lock()
defer table.statesLock.Unlock()
bindMap, ok := table.states[makeUDPStateKey(socketInfo.Local)]
if ok {
udpConnState, ok = bindMap[makeUDPStateKey(remoteAddress)]
return
}
return nil, false
}
func (table *udpTable) getDirection(
socketInfo *socket.BindInfo,
pktInfo *packet.Info,
) (connDirection bool) {
table.statesLock.Lock()
defer table.statesLock.Unlock()
localKey := makeUDPStateKey(socketInfo.Local)
bindMap, ok := table.states[localKey]
if !ok {
bindMap = make(map[string]*udpState)
table.states[localKey] = bindMap
}
remoteKey := makeUDPStateKey(socket.Address{
IP: pktInfo.RemoteIP(),
Port: pktInfo.RemotePort(),
})
udpConnState, ok := bindMap[remoteKey]
if !ok {
bindMap[remoteKey] = &udpState{
inbound: pktInfo.Inbound,
lastSeen: time.Now().UTC(),
}
return pktInfo.Inbound
}
udpConnState.lastSeen = time.Now().UTC()
return udpConnState.inbound
}
func (table *udpTable) cleanStates(now time.Time) {
// compute thresholds
threshold := now.Add(-UDPConnStateTTL)
shortThreshhold := now.Add(-UDPConnStateShortenedTTL)
// make lookup map of all active keys
bindKeys := make(map[string]struct{})
table.lock.RLock()
for _, socketInfo := range table.binds {
bindKeys[makeUDPStateKey(socketInfo.Local)] = struct{}{}
}
table.lock.RUnlock()
table.statesLock.Lock()
defer table.statesLock.Unlock()
// clean the udp state storage
for localKey, bindMap := range table.states {
if _, active := bindKeys[localKey]; active {
// clean old entries
for remoteKey, udpConnState := range bindMap {
if udpConnState.lastSeen.Before(threshold) {
delete(bindMap, remoteKey)
}
}
// if there are too many clean more aggressively
if len(bindMap) > AggressiveCleaningThreshold {
for remoteKey, udpConnState := range bindMap {
if udpConnState.lastSeen.Before(shortThreshhold) {
delete(bindMap, remoteKey)
}
}
}
} else {
// delete the whole thing
delete(table.states, localKey)
}
}
}
func makeUDPStateKey(address socket.Address) string {
// This could potentially go wrong, but as all IPs are created by the same source, everything should be fine.
return string(address.IP) + strconv.Itoa(int(address.Port))
}