From 0e51a9523a9b9979077cf1c09ddaea86c4a20b99 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 23 Feb 2021 13:00:54 +0100 Subject: [PATCH 1/2] Add debug/core endpoint to add more Portmaster debug data --- core/api.go | 40 +++++++++++++++++++++++++++++++++++++++- core/core.go | 2 +- status/module.go | 15 +++++++++++++++ status/security_level.go | 14 +++++++------- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/core/api.go b/core/api.go index bf20f3ba..49cb1428 100644 --- a/core/api.go +++ b/core/api.go @@ -1,13 +1,17 @@ package core import ( + "net/http" + "github.com/safing/portbase/api" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" + "github.com/safing/portbase/utils/debug" + "github.com/safing/portmaster/status" "github.com/safing/portmaster/updates" ) -func registerActions() error { +func registerAPIEndpoints() error { if err := api.RegisterEndpoint(api.Endpoint{ Path: "core/shutdown", Read: api.PermitSelf, @@ -24,6 +28,22 @@ func registerActions() error { return err } + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "debug/core", + Read: api.PermitAnyone, + DataFunc: debugInfo, + Name: "Get Debug Information", + Description: "Returns network debugging information, similar to debug/info, but with system status data.", + Parameters: []api.Parameter{{ + Method: http.MethodGet, + Field: "style", + Value: "github", + Description: "Specify the formatting style. The default is simple markdown formatting.", + }}, + }); err != nil { + return err + } + return nil } @@ -41,3 +61,21 @@ func restart(_ *api.Request) (msg string, err error) { updates.RestartNow() return "restart initiated", nil } + +// debugInfo returns the debugging information for support requests. +func debugInfo(ar *api.Request) (data []byte, err error) { + // Create debug information helper. + di := new(debug.Info) + di.Style = ar.Request.URL.Query().Get("style") + + // Add debug information. + di.AddVersionInfo() + di.AddPlatformInfo(ar.Context()) + status.AddToDebugInfo(di) + di.AddLastReportedModuleError() + di.AddLastUnexpectedLogs() + di.AddGoroutineStack() + + // Return data. + return di.Bytes(), nil +} diff --git a/core/core.go b/core/core.go index 5fb7665b..8674c2a7 100644 --- a/core/core.go +++ b/core/core.go @@ -55,7 +55,7 @@ func start() error { return err } - if err := registerActions(); err != nil { + if err := registerAPIEndpoints(); err != nil { return err } diff --git a/status/module.go b/status/module.go index 05e3c7cc..61a071da 100644 --- a/status/module.go +++ b/status/module.go @@ -2,8 +2,10 @@ package status import ( "context" + "fmt" "github.com/safing/portbase/modules" + "github.com/safing/portbase/utils/debug" "github.com/safing/portmaster/netenv" ) @@ -39,3 +41,16 @@ func start() error { return nil } + +// AddToDebugInfo adds the system status to the given debug.Info. +func AddToDebugInfo(di *debug.Info) { + di.AddSection( + fmt.Sprintf("Status: %s", SecurityLevelString(ActiveSecurityLevel())), + debug.UseCodeSection|debug.AddContentLineBreaks, + fmt.Sprintf("ActiveSecurityLevel: %s", SecurityLevelString(ActiveSecurityLevel())), + fmt.Sprintf("SelectedSecurityLevel: %s", SecurityLevelString(SelectedSecurityLevel())), + fmt.Sprintf("ThreatMitigationLevel: %s", SecurityLevelString(getHighestMitigationLevel())), + fmt.Sprintf("CaptivePortal: %s", netenv.GetCaptivePortal().URL), + fmt.Sprintf("OnlineStatus: %s", netenv.GetOnlineStatus()), + ) +} diff --git a/status/security_level.go b/status/security_level.go index 03a0f4e0..f4770f5d 100644 --- a/status/security_level.go +++ b/status/security_level.go @@ -99,19 +99,19 @@ func SecurityLevelString(level uint8) string { case SecurityLevelOff: return "Off" case SecurityLevelNormal: - return "Normal" + return "Trusted" case SecurityLevelHigh: - return "High" + return "Untrusted" case SecurityLevelExtreme: - return "Extreme" + return "Danger" case SecurityLevelsNormalAndHigh: - return "Normal and High" + return "Trusted and Untrusted" case SecurityLevelsNormalAndExtreme: - return "Normal and Extreme" + return "Trusted and Danger" case SecurityLevelsHighAndExtreme: - return "High and Extreme" + return "Untrusted and Danger" case SecurityLevelsAll: - return "Normal, High and Extreme" + return "Trusted, Untrusted and Danger" default: return "INVALID" } From 253501e1ab9d13187ce6cbd37b7c59ff36eb8f0e Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 23 Feb 2021 13:02:27 +0100 Subject: [PATCH 2/2] Add debug/network endpoint for exposing network data --- network/api.go | 284 ++++++++++++++++++++++++++++++++++++++++++ network/api_test.go | 145 +++++++++++++++++++++ network/connection.go | 4 +- network/module.go | 4 + network/status.go | 4 +- 5 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 network/api.go create mode 100644 network/api_test.go diff --git a/network/api.go b/network/api.go new file mode 100644 index 00000000..f9b2c66c --- /dev/null +++ b/network/api.go @@ -0,0 +1,284 @@ +package network + +import ( + "fmt" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/utils/debug" + "github.com/safing/portmaster/status" +) + +func registerAPIEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: "debug/network", + Read: api.PermitUser, + DataFunc: debugInfo, + Name: "Get Network Debug Information", + Description: "Returns network debugging information, similar to debug/core, but with connection data.", + Parameters: []api.Parameter{ + { + Method: http.MethodGet, + Field: "style", + Value: "github", + Description: "Specify the formatting style. The default is simple markdown formatting.", + }, + { + Method: http.MethodGet, + Field: "profile", + Value: "/", + Description: "Specify a profile source and ID for which network connection should be reported.", + }, + { + Method: http.MethodGet, + Field: "where", + Value: "", + Description: "Specify a query to limit the connections included in the report. The default is to include all connections.", + }, + }, + }); err != nil { + return err + } + + return nil +} + +// debugInfo returns the debugging information for support requests. +func debugInfo(ar *api.Request) (data []byte, err error) { + // Create debug information helper. + di := new(debug.Info) + di.Style = ar.Request.URL.Query().Get("style") + + // Add debug information. + di.AddVersionInfo() + di.AddPlatformInfo(ar.Context()) + status.AddToDebugInfo(di) + AddNetworkDebugData( + di, + ar.Request.URL.Query().Get("profile"), + ar.Request.URL.Query().Get("where"), + ) + di.AddLastReportedModuleError() + di.AddLastUnexpectedLogs() + di.AddGoroutineStack() + + // Return data. + return di.Bytes(), nil +} + +func AddNetworkDebugData(di *debug.Info, profile, where string) { + // Prepend where prefix to query if necessary. + if where != "" && !strings.HasPrefix(where, "where ") { + where = "where " + where + } + + // Build query. + q, err := query.ParseQuery("query network: " + where) + if err != nil { + di.AddSection( + fmt.Sprintf("Network: Debug Failed"), + debug.NoFlags, + fmt.Sprintf("Failed to build query: %s", err), + ) + return + } + + // Get iterator. + it, err := dbController.Query(q, true, true) + if err != nil { + di.AddSection( + fmt.Sprintf("Network: Debug Failed"), + debug.NoFlags, + fmt.Sprintf("Failed to run query: %s", err), + ) + return + } + + // Collect matching connections. + var debugConns []*Connection + var accepted int + var total int + for maybeConn := range it.Next { + // Switch to correct type. + conn, ok := maybeConn.(*Connection) + if !ok { + continue + } + + // Check if the profile matches + if profile != "" { + found := false + + // Get layer IDs and search for a match. + layerIDs := conn.Process().Profile().LayerIDs + for _, layerID := range layerIDs { + if profile == layerID { + found = true + break + } + } + + // Skip if the profile does not match. + if !found { + continue + } + } + + // Count. + total++ + switch conn.Verdict { + case VerdictAccept, + VerdictRerouteToNameserver, + VerdictRerouteToTunnel: + accepted++ + } + + // Add to list. + debugConns = append(debugConns, conn) + } + + // Add it all. + di.AddSection( + fmt.Sprintf( + "Network: %d/%d Connections", + accepted, + total, + ), + debug.UseCodeSection|debug.AddContentLineBreaks, + buildNetworkDebugInfoData(debugConns), + ) +} + +func buildNetworkDebugInfoData(debugConns []*Connection) string { + // Sort + sort.Sort(connectionsByStarted(debugConns)) + + // Format lines + var buf strings.Builder + currentBinaryPath := "__" + for _, conn := range debugConns { + conn.Lock() + + // Add process infomration if it differs from previous connection. + if currentBinaryPath != conn.ProcessContext.BinaryPath { + if currentBinaryPath != "__" { + 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("\nProfile: " + conn.ProcessContext.Profile) + buf.WriteString("\nSource: " + conn.ProcessContext.Source) + buf.WriteString("\n") + + // Set current path in order to not print the process information again. + currentBinaryPath = conn.ProcessContext.BinaryPath + } + + // Add connection. + buf.WriteString("\n") + buf.WriteString(conn.debugInfoLine()) + + conn.Unlock() + } + + return buf.String() +} + +func (conn *Connection) debugInfoLine() string { + var connectionData string + if conn.ID != "" { + // Format IP/Port pair for connections. + connectionData = fmt.Sprintf( + "% 15s:%- 5s %s % 15s:%- 5s", + conn.LocalIP, + strconv.Itoa(int(conn.LocalPort)), + conn.fmtProtocolAndDirectionComponent(conn.IPProtocol.String()), + conn.Entity.IP, + strconv.Itoa(int(conn.Entity.Port)), + ) + } else { + // Leave empty for DNS Requests. + connectionData = " " + } + + return fmt.Sprintf( + "% 14s %s%- 25s %s-%s P#%d [%s] %s - by %s @ %s", + conn.Verdict.Verb(), + connectionData, + conn.fmtDomainComponent(), + time.Unix(conn.Started, 0).Format("15:04:05"), + conn.fmtEndTimeComponent(), + conn.ProcessContext.PID, + conn.fmtFlagsComponent(), + conn.Reason.Msg, + conn.Reason.OptionKey, + conn.fmtReasonProfileComponent(), + ) +} + +func (conn *Connection) fmtDomainComponent() string { + if conn.Entity.Domain != "" { + return " to " + conn.Entity.Domain + } + return "" +} + +func (conn *Connection) fmtProtocolAndDirectionComponent(protocol string) string { + if conn.Inbound { + return "<" + protocol + } + return protocol + ">" +} + +func (conn *Connection) fmtFlagsComponent() string { + var f string + + if conn.Internal { + f += "I" + } + if conn.Encrypted { + f += "E" + } + if conn.Tunneled { + f += "T" + } + if len(conn.activeInspectors) > 0 { + f += "A" + } + if conn.addedToMetrics { + f += "M" + } + + return f +} + +func (conn *Connection) fmtEndTimeComponent() string { + if conn.Ended == 0 { + return " " // Use same width as a timestamp. + } + return time.Unix(conn.Ended, 0).Format("15:04:05") +} + +func (conn *Connection) fmtReasonProfileComponent() string { + if conn.Reason.Profile == "" { + return "global" + } + return conn.Reason.Profile +} + +type connectionsByStarted []*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 { + if a[i].ProcessContext.BinaryPath != a[j].ProcessContext.BinaryPath { + return a[i].ProcessContext.BinaryPath < a[j].ProcessContext.BinaryPath + } + return a[i].Started < a[j].Started +} diff --git a/network/api_test.go b/network/api_test.go new file mode 100644 index 00000000..ae3a1825 --- /dev/null +++ b/network/api_test.go @@ -0,0 +1,145 @@ +package network + +import ( + "fmt" + "net" + "testing" + + "github.com/safing/portmaster/intel" +) + +func TestDebugInfoLineFormatting(t *testing.T) { + for _, conn := range connectionTestData { + fmt.Println(conn.debugInfoLine()) + } +} + +func TestDebugInfoFormatting(t *testing.T) { + fmt.Println(buildNetworkDebugInfoData(connectionTestData)) +} + +var connectionTestData = []*Connection{ + { + ID: "17-255.255.255.255-29810-192.168.0.23-40672", + Scope: "IL", + IPVersion: 4, + Inbound: true, + IPProtocol: 17, + LocalIP: net.ParseIP("255.255.255.255"), + LocalPort: 29810, + Entity: &intel.Entity{ + Protocol: 17, + Port: 40672, + Domain: "", + ReverseDomain: "", + IP: net.ParseIP("192.168.0.23"), + Country: "", + ASN: 0, + }, + Verdict: 4, + Reason: Reason{ + Msg: "incoming connection blocked by default", + OptionKey: "filter/serviceEndpoints", + Profile: "", + }, + Started: 1614010349, + Ended: 1614010350, + VerdictPermanent: true, + Inspecting: false, + Tunneled: false, + Encrypted: false, + ProcessContext: ProcessContext{ + ProcessName: "Unidentified Processes", + ProfileName: "Unidentified Processes", + BinaryPath: "", + PID: -1, + Profile: "_unidentified", + Source: "local", + }, + Internal: false, + ProfileRevisionCounter: 1, + }, + { + ID: "6-192.168.0.176-55216-13.32.6.15-80", + Scope: "PI", + IPVersion: 4, + Inbound: false, + IPProtocol: 6, + LocalIP: net.ParseIP("192.168.0.176"), + LocalPort: 55216, + Entity: &intel.Entity{ + Protocol: 6, + Port: 80, + Domain: "", + ReverseDomain: "", + IP: net.ParseIP("13.32.6.15"), + Country: "DE", + ASN: 16509, + }, + Verdict: 2, + Reason: Reason{ + Msg: "default permit", + OptionKey: "filter/defaultAction", + Profile: "", + }, + Started: 1614010475, + Ended: 1614010565, + VerdictPermanent: true, + Inspecting: false, + Tunneled: false, + Encrypted: false, + ProcessContext: ProcessContext{ + ProcessName: "NetworkManager", + ProfileName: "Network Manager", + BinaryPath: "/usr/sbin/NetworkManager", + PID: 1273, + Profile: "3a9b0eb5-c7fe-4bc7-9b93-a90f4ff84b5b", + Source: "local", + }, + Internal: true, + ProfileRevisionCounter: 1, + }, + { + ID: "6-192.168.0.176-49982-142.250.74.211-443", + Scope: "pkg.go.dev.", + IPVersion: 4, + Inbound: false, + IPProtocol: 6, + LocalIP: net.ParseIP("192.168.0.176"), + LocalPort: 49982, + Entity: &intel.Entity{ + Protocol: 6, + Port: 443, + Domain: "pkg.go.dev.", + ReverseDomain: "", + CNAME: []string{ + "ghs.googlehosted.com.", + }, + IP: net.ParseIP("142.250.74.211"), + Country: "US", + ASN: 15169, + }, + Verdict: 2, + Reason: Reason{ + Msg: "default permit", + OptionKey: "filter/defaultAction", + Profile: "", + }, + Started: 1614010415, + Ended: 1614010745, + VerdictPermanent: true, + Inspecting: false, + Tunneled: false, + Encrypted: false, + ProcessContext: ProcessContext{ + ProcessName: "firefox", + ProfileName: "Firefox", + BinaryPath: "/usr/bin/firefox", + PID: 5710, + Profile: "74b30392-9e4d-4157-83a9-fffafd3e2bde", + Source: "local", + }, + Internal: false, + ProfileRevisionCounter: 1, + }, +} diff --git a/network/connection.go b/network/connection.go index f8b130e9..750de2a6 100644 --- a/network/connection.go +++ b/network/connection.go @@ -33,7 +33,7 @@ type ProcessContext struct { ProfileName string // BinaryPath is the path to the process binary. BinaryPath string - // PID i the process identifier. + // PID is the process identifier. PID int // Profile is the ID of the main profile that // is applied to the process. @@ -93,7 +93,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment Reason Reason // Started holds the number of seconds in UNIX epoch time at which // the connection has been initated and first seen by the portmaster. - // Staretd is only every set when creating a new connection object + // 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 diff --git a/network/module.go b/network/module.go index e3ed28b0..2d4f6936 100644 --- a/network/module.go +++ b/network/module.go @@ -31,6 +31,10 @@ func start() error { return err } + if err := registerAPIEndpoints(); err != nil { + return err + } + module.StartServiceWorker("clean connections", 0, connectionCleaner) module.StartServiceWorker("write open dns requests", 0, openDNSRequestWriter) diff --git a/network/status.go b/network/status.go index e0d1042b..75c7f5a8 100644 --- a/network/status.go +++ b/network/status.go @@ -54,9 +54,9 @@ func (v Verdict) Verb() string { case VerdictDrop: return "dropped" case VerdictRerouteToNameserver: - return "rerouted to nameserver" + return "to nameserver" case VerdictRerouteToTunnel: - return "rerouted to tunnel" + return "to tunnel" case VerdictFailed: return "failed" default: