diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 3a476f8a..ad1cff37 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -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. diff --git a/network/api.go b/network/api.go index f9b2c66c..d828394e 100644 --- a/network/api.go +++ b/network/api.go @@ -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 } diff --git a/network/connection.go b/network/connection.go index 6967c2e2..dda54416 100644 --- a/network/connection.go +++ b/network/connection.go @@ -29,10 +29,12 @@ type FirewallHandler func(conn *Connection, pkt packet.Packet) type ProcessContext struct { // ProcessName is the name of the process. ProcessName string - //ProfileName is the name of the profile. + // 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 // 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,7 +276,10 @@ func NewConnectionFromExternalDNSRequest(ctx context.Context, fqdn string, cname timestamp := time.Now().Unix() dnsConn := &Connection{ - Scope: fqdn, + ID: connID, + Type: DNSRequest, + External: true, + Scope: fqdn, Entity: &intel.Entity{ Domain: fqdn, CNAME: cnames, @@ -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) } } diff --git a/network/connection_store.go b/network/connection_store.go index 18b753ea..a3c633cb 100644 --- a/network/connection_store.go +++ b/network/connection_store.go @@ -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) { diff --git a/network/database.go b/network/database.go index 3a13163b..6d9a337d 100644 --- a/network/database.go +++ b/network/database.go @@ -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,37 +26,86 @@ type StorageInterface struct { storage.InjectBase } -// Get returns a database record. -func (s *StorageInterface) Get(key string) (record.Record, error) { +// Database prefixes: +// Processes: network:tree/ +// DNS Requests: network:tree//dns/ +// IP Connections: network:tree//ip/ - 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 { - 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 +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 } } - case "system": - if len(splitted) >= 2 { - switch splitted[1] { - case "state": - return state.GetInfo(), nil - default: - } + + 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 + } + + 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 } } @@ -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() diff --git a/process/special.go b/process/special.go index 24e3aa8b..8ec47b4c 100644 --- a/process/special.go +++ b/process/special.go @@ -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 )