Merge pull request #261 from safing/feature/debug-info

Add Portmaster debug endpoints
This commit is contained in:
Patrick Pacher 2021-02-23 16:53:05 +01:00 committed by GitHub
commit d13cfb1bb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 499 additions and 13 deletions

View file

@ -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
}

View file

@ -55,7 +55,7 @@ func start() error {
return err
}
if err := registerActions(); err != nil {
if err := registerAPIEndpoints(); err != nil {
return err
}

284
network/api.go Normal file
View file

@ -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: "<Source>/<ID>",
Description: "Specify a profile source and ID for which network connection should be reported.",
},
{
Method: http.MethodGet,
Field: "where",
Value: "<query>",
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
}

145
network/api_test.go Normal file
View file

@ -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,
},
}

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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()),
)
}

View file

@ -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"
}