safing-portmaster/service/network/api.go
2024-03-27 16:17:58 +01:00

340 lines
8.3 KiB
Go

package network
import (
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/safing/portbase/api"
"github.com/safing/portbase/config"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/utils/debug"
"github.com/safing/portmaster/service/network/state"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/resolver"
"github.com/safing/portmaster/service/status"
"github.com/safing/portmaster/service/updates"
)
func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Path: "debug/network",
Read: api.PermitUser,
BelongsTo: module,
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
}
if err := api.RegisterEndpoint(api.Endpoint{
Path: "debug/network/state",
Read: api.PermitUser,
BelongsTo: module,
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
}
// 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.
// Very basic information at the start.
di.AddVersionInfo()
di.AddPlatformInfo(ar.Context())
// Errors and unexpected logs.
di.AddLastReportedModuleError()
di.AddLastUnexpectedLogs()
// Network Connections.
AddNetworkDebugData(
di,
ar.Request.URL.Query().Get("profile"),
ar.Request.URL.Query().Get("where"),
)
// Status Information from various modules.
status.AddToDebugInfo(di)
// captain.AddToDebugInfo(di) // TODO: Cannot use due to import loop.
resolver.AddToDebugInfo(di)
config.AddToDebugInfo(di)
// Detailed information.
updates.AddToDebugInfo(di)
// compat.AddToDebugInfo(di) // TODO: Cannot use due to interception import requirement which we don't want for SPN Hubs.
di.AddGoroutineStack()
// Return data.
return di.Bytes(), nil
}
// AddNetworkDebugData adds the network debug data of the given profile to the debug data.
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(
"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(
"Network: Debug Failed",
debug.NoFlags,
fmt.Sprintf("Failed to run query: %s", err),
)
return
}
// Collect matching connections.
var ( //nolint:prealloc // We don't know the size.
debugConns []*Connection
accepted int
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 { //nolint:exhaustive
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(connectionsByGroup(debugConns))
// Format lines
var buf strings.Builder
currentPID := process.UndefinedProcessID
for _, conn := range debugConns {
conn.Lock()
// Add process infomration if it differs from previous connection.
if currentPID != conn.ProcessContext.PID {
if currentPID != process.UndefinedProcessID {
buf.WriteString("\n\n\n")
}
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 PID in order to not print the process information again.
currentPID = conn.ProcessContext.PID
}
// Add connection.
buf.WriteString("\n")
buf.WriteString(conn.debugInfoLine())
conn.Unlock()
}
return buf.String()
}
func (conn *Connection) debugInfoLine() string {
var connectionData string
if conn.Type == IPConnection {
// 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.VerdictVerb(),
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 connectionsByGroup []*Connection
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
}