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/network/state" "github.com/safing/portmaster/process" "github.com/safing/portmaster/resolver" "github.com/safing/portmaster/status" "github.com/safing/portmaster/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: "/", 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 } 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 transitioning 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.Firewall { //nolint:exhaustive case VerdictAccept, VerdictRerouteToNameserver, VerdictRerouteToTunnel: accepted++ } if conn.Verdict.Active != conn.Verdict.Firewall { transitioning++ } // Add to list. debugConns = append(debugConns, conn) } // Add it all. di.AddSection( fmt.Sprintf( "Network: %d/%d [~%d] Connections", accepted, total, transitioning, ), 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 }