mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
Merge pull request #261 from safing/feature/debug-info
Add Portmaster debug endpoints
This commit is contained in:
commit
d13cfb1bb4
9 changed files with 499 additions and 13 deletions
40
core/api.go
40
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
|
||||
}
|
||||
|
|
|
@ -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
284
network/api.go
Normal 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
145
network/api_test.go
Normal 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,
|
||||
},
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue