Revamp Connection.ID

Add Connection.Type and Connection.External
Deprecate Connection.Scope
This commit is contained in:
Daniel 2021-03-20 22:29:29 +01:00
parent 20383226f8
commit fbf666ee68
6 changed files with 196 additions and 76 deletions

View file

@ -100,18 +100,27 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
// Authenticate request - only requests from the local host, but with any of its IPs, are allowed.
local, err := netenv.IsMyIP(remoteAddr.IP)
if err != nil {
tracer.Warningf("nameserver: failed to check if request for %s%s is local: %s", q.FQDN, q.QType, err)
tracer.Warningf("nameserver: failed to check if request for %s is local: %s", q.ID(), err)
return nil // Do no reply, drop request immediately.
}
// Create connection ID for dns request.
connID := fmt.Sprintf(
"%s-%d-#%d-%s",
remoteAddr.IP,
remoteAddr.Port,
request.Id,
q.ID(),
)
// Get connection for this request. This identifies the process behind the request.
var conn *network.Connection
switch {
case local:
conn = network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, remoteAddr.IP, uint16(remoteAddr.Port))
conn = network.NewConnectionFromDNSRequest(ctx, q.FQDN, nil, connID, remoteAddr.IP, uint16(remoteAddr.Port))
case networkServiceMode():
conn, err = network.NewConnectionFromExternalDNSRequest(ctx, q.FQDN, nil, remoteAddr.IP)
conn, err = network.NewConnectionFromExternalDNSRequest(ctx, q.FQDN, nil, connID, remoteAddr.IP)
if err != nil {
tracer.Warningf("nameserver: failed to get host/profile for request for %s%s: %s", q.FQDN, q.QType, err)
return nil // Do no reply, drop request immediately.

View file

@ -11,6 +11,8 @@ import (
"github.com/safing/portbase/api"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/utils/debug"
"github.com/safing/portmaster/network/state"
"github.com/safing/portmaster/process"
"github.com/safing/portmaster/status"
)
@ -45,6 +47,18 @@ func registerAPIEndpoints() error {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: "debug/network/state",
Read: api.PermitUser,
StructFunc: func(ar *api.Request) (i interface{}, err error) {
return state.GetInfo(), nil
},
Name: "Get Network State Table Data",
Description: "Returns the current network state tables from the OS.",
}); err != nil {
return err
}
return nil
}
@ -156,28 +170,30 @@ func AddNetworkDebugData(di *debug.Info, profile, where string) {
func buildNetworkDebugInfoData(debugConns []*Connection) string {
// Sort
sort.Sort(connectionsByStarted(debugConns))
sort.Sort(connectionsByGroup(debugConns))
// Format lines
var buf strings.Builder
currentBinaryPath := "__"
currentPID := process.UndefinedProcessID
for _, conn := range debugConns {
conn.Lock()
// Add process infomration if it differs from previous connection.
if currentBinaryPath != conn.ProcessContext.BinaryPath {
if currentBinaryPath != "__" {
if currentPID != conn.ProcessContext.PID {
if currentPID != process.UndefinedProcessID {
buf.WriteString("\n\n\n")
}
buf.WriteString("ProcessName: " + conn.ProcessContext.ProcessName)
buf.WriteString("\nProfileName: " + conn.ProcessContext.ProfileName)
buf.WriteString("\nBinaryPath: " + conn.ProcessContext.BinaryPath)
buf.WriteString("ProfileName: " + conn.ProcessContext.ProfileName)
buf.WriteString("\nProfile: " + conn.ProcessContext.Profile)
buf.WriteString("\nSource: " + conn.ProcessContext.Source)
buf.WriteString("\nProcessName: " + conn.ProcessContext.ProcessName)
buf.WriteString("\nBinaryPath: " + conn.ProcessContext.BinaryPath)
buf.WriteString("\nCmdLine: " + conn.ProcessContext.CmdLine)
buf.WriteString("\nPID: " + strconv.Itoa(conn.ProcessContext.PID))
buf.WriteString("\n")
// Set current path in order to not print the process information again.
currentBinaryPath = conn.ProcessContext.BinaryPath
// Set current PID in order to not print the process information again.
currentPID = conn.ProcessContext.PID
}
// Add connection.
@ -192,7 +208,7 @@ func buildNetworkDebugInfoData(debugConns []*Connection) string {
func (conn *Connection) debugInfoLine() string {
var connectionData string
if conn.ID != "" {
if conn.Type == DNSRequest { // conn.ID !=
// Format IP/Port pair for connections.
connectionData = fmt.Sprintf(
"% 15s:%- 5s %s % 15s:%- 5s",
@ -272,13 +288,28 @@ func (conn *Connection) fmtReasonProfileComponent() string {
return conn.Reason.Profile
}
type connectionsByStarted []*Connection
type connectionsByGroup []*Connection
func (a connectionsByStarted) Len() int { return len(a) }
func (a connectionsByStarted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a connectionsByStarted) Less(i, j int) bool {
func (a connectionsByGroup) Len() int { return len(a) }
func (a connectionsByGroup) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a connectionsByGroup) Less(i, j int) bool {
// Sort by:
// 1. Profile ID
if a[i].ProcessContext.Profile != a[j].ProcessContext.Profile {
return a[i].ProcessContext.Profile < a[j].ProcessContext.Profile
}
// 2. Process Binary
if a[i].ProcessContext.BinaryPath != a[j].ProcessContext.BinaryPath {
return a[i].ProcessContext.BinaryPath < a[j].ProcessContext.BinaryPath
}
// 3. Process ID
if a[i].ProcessContext.PID != a[j].ProcessContext.PID {
return a[i].ProcessContext.PID < a[j].ProcessContext.PID
}
// 4. Started
return a[i].Started < a[j].Started
}

View file

@ -33,6 +33,8 @@ type ProcessContext struct {
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
// Profile is the ID of the main profile that
@ -42,21 +44,37 @@ type ProcessContext struct {
Source string
}
type ConnectionType int8
const (
Undefined ConnectionType = iota
IPConnection
DNSRequest
// ProxyRequest
)
// 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 may hold unique connection id. It is only set for non-DNS
// request connections and is considered immutable after a
// connection object has been created.
// 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.
@ -176,8 +194,9 @@ type Reason struct {
func getProcessContext(ctx context.Context, proc *process.Process) ProcessContext {
// Gather process information.
pCtx := ProcessContext{
BinaryPath: proc.Path,
ProcessName: proc.Name,
BinaryPath: proc.Path,
CmdLine: proc.CmdLine,
PID: proc.Pid,
}
@ -196,7 +215,7 @@ func getProcessContext(ctx context.Context, proc *process.Process) ProcessContex
}
// NewConnectionFromDNSRequest returns a new connection based on the given dns request.
func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []string, localIP net.IP, localPort uint16) *Connection {
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 {
@ -223,6 +242,8 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []stri
timestamp := time.Now().Unix()
dnsConn := &Connection{
ID: connID,
Type: DNSRequest,
Scope: fqdn,
Entity: &intel.Entity{
Domain: fqdn,
@ -239,10 +260,15 @@ func NewConnectionFromDNSRequest(ctx context.Context, fqdn string, cnames []stri
dnsConn.Internal = localProfile.Internal
}
// Always mark dns queries from the system resolver as internal.
if proc.IsSystemResolver() {
dnsConn.Internal = true
}
return dnsConn
}
func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cnames []string, remoteIP net.IP) (*Connection, error) {
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
@ -250,6 +276,9 @@ func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cname
timestamp := time.Now().Unix()
dnsConn := &Connection{
ID: connID,
Type: DNSRequest,
External: true,
Scope: fqdn,
Entity: &intel.Entity{
Domain: fqdn,
@ -280,6 +309,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
var scope string
var entity *intel.Entity
var resolverInfo *resolver.ResolverInfo
if inbound {
@ -316,7 +346,11 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
entity.SetDstPort(entity.Port)
// check if we can find a domain for that IP
ipinfo, err := resolver.GetIPInfo(proc.LocalProfileKey, pkt.Info().Dst.String())
ipinfo, err := resolver.GetIPInfo(proc.Profile().LocalProfile().ID, pkt.Info().Dst.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().Dst.String())
}
if err == nil {
lastResolvedDomain := ipinfo.MostRecentDomain()
if lastResolvedDomain != nil {
@ -358,6 +392,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection {
// Create new connection object.
newConn := &Connection{
ID: pkt.GetConnectionID(),
Type: IPConnection,
Scope: scope,
IPVersion: pkt.Info().Version,
Inbound: inbound,
@ -493,14 +528,11 @@ func (conn *Connection) Save() {
conn.UpdateMeta()
if !conn.KeyIsSet() {
// A connection without an ID has been created from
// a DNS request rather than a packet. Choose the correct
// connection store here.
if conn.ID == "" {
conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Scope))
if conn.Type == DNSRequest {
conn.SetKey(makeKey(conn.process.Pid, "dns", conn.ID))
dnsConns.add(conn)
} else {
conn.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", conn.process.Pid, conn.Scope, conn.ID))
conn.SetKey(makeKey(conn.process.Pid, "ip", conn.ID))
conns.add(conn)
}
}

View file

@ -1,7 +1,6 @@
package network
import (
"strconv"
"sync"
)
@ -16,25 +15,18 @@ func newConnectionStore() *connectionStore {
}
}
func (cs *connectionStore) getID(conn *Connection) string {
if conn.ID != "" {
return conn.ID
}
return strconv.Itoa(conn.process.Pid) + "/" + conn.Scope
}
func (cs *connectionStore) add(conn *Connection) {
cs.rw.Lock()
defer cs.rw.Unlock()
cs.items[cs.getID(conn)] = conn
cs.items[conn.ID] = conn
}
func (cs *connectionStore) delete(conn *Connection) {
cs.rw.Lock()
defer cs.rw.Unlock()
delete(cs.items, cs.getID(conn))
delete(cs.items, conn.ID)
}
func (cs *connectionStore) get(id string) (*Connection, bool) {

View file

@ -1,11 +1,10 @@
package network
import (
"fmt"
"strconv"
"strings"
"github.com/safing/portmaster/network/state"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/iterator"
"github.com/safing/portbase/database/query"
@ -27,39 +26,88 @@ type StorageInterface struct {
storage.InjectBase
}
// Database prefixes:
// Processes: network:tree/<PID>
// DNS Requests: network:tree/<PID>/dns/<ID>
// IP Connections: network:tree/<PID>/ip/<ID>
func makeKey(pid int, scope, id string) string {
if scope == "" {
return "network:tree/" + strconv.Itoa(pid)
}
return fmt.Sprintf("network:tree/%d/%s/%s", pid, scope, id)
}
func parseDBKey(key string) (pid int, scope, id string, ok bool) {
// Split into segments.
segments := strings.Split(key, "/")
// Check for valid prefix.
if !strings.HasPrefix("tree", segments[0]) {
return 0, "", "", false
}
// Keys have 2 or 4 segments.
switch len(segments) {
case 4:
id = segments[3]
fallthrough
case 3:
scope = segments[2]
// Sanity check.
switch scope {
case "dns", "ip", "":
// Parsed id matches possible values.
// The empty string is for matching a trailing slash for in query prefix.
// TODO: For queries, also prefixes of these values are valid.
default:
// Unknown scope.
return 0, "", "", false
}
fallthrough
case 2:
var err error
if segments[1] == "" {
pid = process.UndefinedProcessID
} else {
pid, err = strconv.Atoi(segments[1])
if err != nil {
return 0, "", "", false
}
}
return pid, scope, id, true
case 1:
// This is a valid query prefix, but not process ID was given.
return process.UndefinedProcessID, "", "", true
default:
return 0, "", "", false
}
}
// Get returns a database record.
func (s *StorageInterface) Get(key string) (record.Record, error) {
// Parse key and check if valid.
pid, scope, id, ok := parseDBKey(strings.TrimPrefix(key, "network:"))
if !ok || pid == process.UndefinedProcessID {
return nil, storage.ErrNotFound
}
splitted := strings.Split(key, "/")
switch splitted[0] { //nolint:gocritic // TODO: implement full key space
case "tree":
switch len(splitted) {
case 2:
pid, err := strconv.Atoi(splitted[1])
if err == nil {
proc, ok := process.GetProcessFromStorage(pid)
if ok {
switch scope {
case "dns":
if r, ok := dnsConns.get(id); ok {
return r, nil
}
case "ip":
if r, ok := conns.get(id); ok {
return r, nil
}
case "":
if proc, ok := process.GetProcessFromStorage(pid); ok {
return proc, nil
}
}
case 3:
if r, ok := dnsConns.get(splitted[1] + "/" + splitted[2]); ok {
return r, nil
}
case 4:
if r, ok := conns.get(splitted[3]); ok {
return r, nil
}
}
case "system":
if len(splitted) >= 2 {
switch splitted[1] {
case "state":
return state.GetInfo(), nil
default:
}
}
}
return nil, storage.ErrNotFound
}
@ -74,9 +122,13 @@ func (s *StorageInterface) Query(q *query.Query, local, internal bool) (*iterato
}
func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
slashes := strings.Count(q.DatabaseKeyPrefix(), "/")
pid, scope, _, ok := parseDBKey(q.DatabaseKeyPrefix())
if !ok {
it.Finish(nil)
return
}
if slashes <= 1 {
if pid == process.UndefinedProcessID {
// processes
for _, proc := range process.All() {
proc.Lock()
@ -87,7 +139,7 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
}
}
if slashes <= 2 {
if scope == "" || scope == "dns" {
// dns scopes only
for _, dnsConn := range dnsConns.clone() {
dnsConn.Lock()
@ -98,7 +150,7 @@ func (s *StorageInterface) processQuery(q *query.Query, it *iterator.Iterator) {
}
}
if slashes <= 3 {
if scope == "" || scope == "ip" {
// connections
for _, conn := range conns.clone() {
conn.Lock()

View file

@ -14,6 +14,10 @@ const (
// attributed to a PID for any reason.
UnidentifiedProcessID = -1
// UndefinedProcessID is not used by any (virtual) process and signifies that
// the PID is unset.
UndefinedProcessID = -2
// NetworkHostProcessID is the PID used for requests served to the network.
NetworkHostProcessID = -255
)