wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
102
service/network/state/exists.go
Normal file
102
service/network/state/exists.go
Normal 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
|
||||
}
|
||||
38
service/network/state/info.go
Normal file
38
service/network/state/info.go
Normal 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
|
||||
}
|
||||
264
service/network/state/lookup.go
Normal file
264
service/network/state/lookup.go
Normal 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
|
||||
}
|
||||
46
service/network/state/system_default.go
Normal file
46
service/network/state/system_default.go
Normal 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
|
||||
}
|
||||
40
service/network/state/system_linux.go
Normal file
40
service/network/state/system_linux.go
Normal 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
|
||||
}
|
||||
19
service/network/state/system_windows.go
Normal file
19
service/network/state/system_windows.go
Normal 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
|
||||
}
|
||||
91
service/network/state/tcp.go
Normal file
91
service/network/state/tcp.go
Normal 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()
|
||||
}
|
||||
210
service/network/state/udp.go
Normal file
210
service/network/state/udp.go
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user