diff --git a/cmd/system-probe/api/debug/handlers_linux.go b/cmd/system-probe/api/debug/handlers_linux.go index 07ba06c49354f..54737415699de 100644 --- a/cmd/system-probe/api/debug/handlers_linux.go +++ b/cmd/system-probe/api/debug/handlers_linux.go @@ -9,14 +9,110 @@ package debug import ( + "bytes" "context" "errors" "fmt" + "io" "net/http" "os/exec" + "regexp" + "strconv" + "syscall" "time" + + "golang.org/x/sys/unix" ) +var klogRegexp = regexp.MustCompile(`<(\d+)>(.*)`) + +var klogLevels = []string{ + "emerg", + "alert", + "crit", + "err", + "warn", + "notice", + "info", + "debug", +} + +// lowest 3 bits are the log level, remaining bits are the facility +const klogFacilityShift = 3 +const klogLevelMask = (1 << klogFacilityShift) - 1 + +func klogLevelName(level int) string { + return klogLevels[level&klogLevelMask] +} + +func readAllDmesg() ([]byte, error) { + n, err := syscall.Klogctl(unix.SYSLOG_ACTION_SIZE_BUFFER, nil) + if err != nil { + return nil, fmt.Errorf("failed to query size of log buffer [%w]", err) + } + + b := make([]byte, n) + + m, err := syscall.Klogctl(unix.SYSLOG_ACTION_READ_ALL, b) + if err != nil { + return nil, fmt.Errorf("failed to read messages from log buffer [%w]", err) + } + + return b[:m], nil +} + +func parseDmesg(buffer []byte) (string, error) { + buf := bytes.NewBuffer(buffer) + var result string + + for { + line, err := buf.ReadString('\n') + if err == io.EOF { + break + } else if err != nil { + return result, err + } + + levelName := "info" + message := line + + // convert the numeric log level to a string + parts := klogRegexp.FindStringSubmatch(line) + if parts != nil { + message = parts[2] + + digits := parts[1] + level, err := strconv.Atoi(digits) + if err == nil { + levelName = klogLevelName(level) + } + } + + result += fmt.Sprintf("%-6s: %s\n", levelName, message) + } + + return result, nil +} + +// HandleLinuxDmesg writes linux dmesg into the HTTP response. +func HandleLinuxDmesg(w http.ResponseWriter, _ *http.Request) { + dmesg, err := readAllDmesg() + if err != nil { + w.WriteHeader(500) + fmt.Fprintf(w, "failed to read dmesg: %s", err) + return + } + + dmesgStr, err := parseDmesg(dmesg) + if err != nil { + w.WriteHeader(500) + fmt.Fprintf(w, "failed to parse dmesg: %s", err) + return + } + + io.WriteString(w, dmesgStr) +} + // handleCommand runs commandName with the provided arguments and writes it to the HTTP response. // If the command exits with a failure or doesn't exist in the PATH, it will still 200 but report the failure. // Any other kind of error will 500. diff --git a/cmd/system-probe/api/debug/handlers_linux_test.go b/cmd/system-probe/api/debug/handlers_linux_test.go new file mode 100644 index 0000000000000..3f2d20a761134 --- /dev/null +++ b/cmd/system-probe/api/debug/handlers_linux_test.go @@ -0,0 +1,24 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +//go:build linux + +// Package debug contains handlers for debug information global to all of system-probe +package debug + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const userFacility = 8 + +func TestKlogLevelName(t *testing.T) { + require.Equal(t, "emerg", klogLevelName(0)) + require.Equal(t, "notice", klogLevelName(5)) + + require.Equal(t, "notice", klogLevelName(userFacility|5)) +} diff --git a/cmd/system-probe/api/debug/handlers_nolinux.go b/cmd/system-probe/api/debug/handlers_nolinux.go index 246f4a3a7c78a..1e8a84189e07c 100644 --- a/cmd/system-probe/api/debug/handlers_nolinux.go +++ b/cmd/system-probe/api/debug/handlers_nolinux.go @@ -13,6 +13,12 @@ import ( "net/http" ) +// HandleLinuxDmesg is not supported +func HandleLinuxDmesg(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(500) + io.WriteString(w, "HandleLinuxDmesg is not supported on this platform") +} + // HandleSelinuxSestatus is not supported func HandleSelinuxSestatus(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(500) diff --git a/cmd/system-probe/api/server.go b/cmd/system-probe/api/server.go index f4d9e85522d91..f0fbe81919f30 100644 --- a/cmd/system-probe/api/server.go +++ b/cmd/system-probe/api/server.go @@ -59,6 +59,7 @@ func StartServer(cfg *sysconfigtypes.Config, telemetry telemetry.Component, wmet if runtime.GOOS == "linux" { mux.HandleFunc("/debug/ebpf_btf_loader_info", ebpf.HandleBTFLoaderInfo) + mux.HandleFunc("/debug/dmesg", debug.HandleLinuxDmesg) mux.HandleFunc("/debug/selinux_sestatus", debug.HandleSelinuxSestatus) mux.HandleFunc("/debug/selinux_semodule_list", debug.HandleSelinuxSemoduleList) } diff --git a/pkg/flare/archive_linux.go b/pkg/flare/archive_linux.go index 9a3aea87a0ac0..54bde1e4e7a83 100644 --- a/pkg/flare/archive_linux.go +++ b/pkg/flare/archive_linux.go @@ -8,12 +8,7 @@ package flare import ( - "bytes" - "fmt" - "io" "path/filepath" - "regexp" - "syscall" "github.com/DataDog/ebpf-manager/tracefs" @@ -38,11 +33,24 @@ func addSystemProbePlatformSpecificEntries(fb flaretypes.FlareBuilder) { _ = fb.AddFileFromFunc(filepath.Join("system-probe", "conntrack_cached.log"), getSystemProbeConntrackCached) _ = fb.AddFileFromFunc(filepath.Join("system-probe", "conntrack_host.log"), getSystemProbeConntrackHost) _ = fb.AddFileFromFunc(filepath.Join("system-probe", "ebpf_btf_loader.log"), getSystemProbeBTFLoaderInfo) + _ = fb.AddFileFromFunc(filepath.Join("system-probe", "dmesg.log"), getLinuxDmesg) _ = fb.AddFileFromFunc(filepath.Join("system-probe", "selinux_sestatus.log"), getSystemProbeSelinuxSestatus) _ = fb.AddFileFromFunc(filepath.Join("system-probe", "selinux_semodule_list.log"), getSystemProbeSelinuxSemoduleList) } } +// only used in tests when running on linux +var linuxKernelSymbols = getLinuxKernelSymbols + +func addSecurityAgentPlatformSpecificEntries(fb flaretypes.FlareBuilder) { + linuxKernelSymbols(fb) //nolint:errcheck + getLinuxPid1MountInfo(fb) //nolint:errcheck + fb.AddFileFromFunc("dmesg", getLinuxDmesg) //nolint:errcheck + getLinuxKprobeEvents(fb) //nolint:errcheck + getLinuxTracingAvailableEvents(fb) //nolint:errcheck + getLinuxTracingAvailableFilterFunctions(fb) //nolint:errcheck +} + func getLinuxKernelSymbols(fb flaretypes.FlareBuilder) error { return fb.CopyFile("/proc/kallsyms") } @@ -59,62 +67,10 @@ func getLinuxPid1MountInfo(fb flaretypes.FlareBuilder) error { return fb.CopyFile("/proc/1/mountinfo") } -var klogRegexp = regexp.MustCompile(`<(\d+)>(.*)`) - -func readAllDmesg() ([]byte, error) { - const syslogActionSizeBuffer = 10 - const syslogActionReadAll = 3 - - n, err := syscall.Klogctl(syslogActionSizeBuffer, nil) - if err != nil { - return nil, fmt.Errorf("failed to query size of log buffer [%w]", err) - } - - b := make([]byte, n) - - m, err := syscall.Klogctl(syslogActionReadAll, b) - if err != nil { - return nil, fmt.Errorf("failed to read messages from log buffer [%w]", err) - } - - return b[:m], nil -} - -func parseDmesg(buffer []byte) (string, error) { - buf := bytes.NewBuffer(buffer) - var result string - - for { - line, err := buf.ReadString('\n') - if err == io.EOF { - break - } else if err != nil { - return result, err - } - - parts := klogRegexp.FindStringSubmatch(line) - if parts != nil { - result += parts[2] + "\n" - } else { - result += line - } - } - - return result, nil -} - -func getLinuxDmesg(fb flaretypes.FlareBuilder) error { - dmesg, err := readAllDmesg() - if err != nil { - return err - } - - content, err := parseDmesg(dmesg) - if err != nil { - return err - } - - return fb.AddFile("dmesg", []byte(content)) +func getLinuxDmesg() ([]byte, error) { + sysProbeClient := sysprobeclient.Get(getSystemProbeSocketPath()) + url := sysprobeclient.DebugURL("/dmesg") + return getHTTPData(sysProbeClient, url) } func getLinuxTracingAvailableEvents(fb flaretypes.FlareBuilder) error { diff --git a/pkg/flare/archive_nolinux.go b/pkg/flare/archive_nolinux.go index 9b78a31e1f068..effc92e7b1058 100644 --- a/pkg/flare/archive_nolinux.go +++ b/pkg/flare/archive_nolinux.go @@ -13,26 +13,11 @@ import ( func addSystemProbePlatformSpecificEntries(_ flaretypes.FlareBuilder) {} -func getLinuxKernelSymbols(_ flaretypes.FlareBuilder) error { - return nil -} +func addSecurityAgentPlatformSpecificEntries(_ flaretypes.FlareBuilder) {} -func getLinuxKprobeEvents(_ flaretypes.FlareBuilder) error { - return nil -} - -func getLinuxDmesg(_ flaretypes.FlareBuilder) error { - return nil -} - -func getLinuxPid1MountInfo(_ flaretypes.FlareBuilder) error { - return nil -} - -func getLinuxTracingAvailableEvents(_ flaretypes.FlareBuilder) error { - return nil -} +// only used in tests when running on linux +var linuxKernelSymbols = getLinuxKernelSymbols //nolint:unused -func getLinuxTracingAvailableFilterFunctions(_ flaretypes.FlareBuilder) error { +func getLinuxKernelSymbols(_ flaretypes.FlareBuilder) error { //nolint:unused return nil } diff --git a/pkg/flare/archive_security.go b/pkg/flare/archive_security.go index 1fb01ba7e3aa1..e45377bb8270d 100644 --- a/pkg/flare/archive_security.go +++ b/pkg/flare/archive_security.go @@ -15,9 +15,6 @@ import ( "github.com/DataDog/datadog-agent/pkg/util/log" ) -// for testing purpose -var linuxKernelSymbols = getLinuxKernelSymbols - // CreateSecurityAgentArchive packages up the files func CreateSecurityAgentArchive(local bool, logFilePath string, statusComponent status.Component) (string, error) { fb, err := flarehelpers.NewFlareBuilder(local, flaretypes.FlareArgs{}) @@ -52,12 +49,8 @@ func createSecurityAgentArchive(fb flaretypes.FlareBuilder, logFilePath string, getRuntimeFiles(fb) //nolint:errcheck getExpVar(fb) //nolint:errcheck fb.AddFileFromFunc("envvars.log", getEnvVars) //nolint:errcheck - linuxKernelSymbols(fb) //nolint:errcheck - getLinuxPid1MountInfo(fb) //nolint:errcheck - getLinuxDmesg(fb) //nolint:errcheck - getLinuxKprobeEvents(fb) //nolint:errcheck - getLinuxTracingAvailableEvents(fb) //nolint:errcheck - getLinuxTracingAvailableFilterFunctions(fb) //nolint:errcheck + + addSecurityAgentPlatformSpecificEntries(fb) } func getComplianceFiles(fb flaretypes.FlareBuilder) error { diff --git a/releasenotes/notes/agent-flare-dmesg-d1de3cbb876c05d8.yaml b/releasenotes/notes/agent-flare-dmesg-d1de3cbb876c05d8.yaml new file mode 100644 index 0000000000000..99f496848765f --- /dev/null +++ b/releasenotes/notes/agent-flare-dmesg-d1de3cbb876c05d8.yaml @@ -0,0 +1,11 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +enhancements: + - | + Added the Linux kernel's dmesg logs into the Agent flare. This information will appear in ``system-probe/dmesg.log``. \ No newline at end of file