From 396b9b1d04bc02ef8fb7f83096997abc476aad58 Mon Sep 17 00:00:00 2001 From: John Fastabend Date: Tue, 11 Jun 2024 11:32:39 -0700 Subject: [PATCH] tetragon: Add debug interface to track cgroups to namespace mappings Debugging BPF and some kernel functions I want to understand cgroup to namespace mappings at event side. This patch maintains a stable mapping between cgroups and human readable namespaces. The end goal is to filter out noisy namespaces from execs which will be follow up series. This is minimally useful as is. To support this just extend the use of namespace filters from kprobe and tracepoints into a more general space where we can hook selectors. Signed-off-by: John Fastabend --- bpf/process/bpf_execve_event.c | 2 + bpf/process/policy_filter.h | 8 ++ cmd/tetra/dump/dump.go | 25 ++++ cmd/tetra/policyfilter/policyfilter.go | 17 +++ pkg/policyfilter/namespace.go | 157 +++++++++++++++++++++++++ pkg/policyfilter/state.go | 13 ++ 6 files changed, 222 insertions(+) create mode 100644 pkg/policyfilter/namespace.go diff --git a/bpf/process/bpf_execve_event.c b/bpf/process/bpf_execve_event.c index 0d05447a17c..f190c3498ac 100644 --- a/bpf/process/bpf_execve_event.c +++ b/bpf/process/bpf_execve_event.c @@ -11,6 +11,8 @@ #include "bpf_helpers.h" #include "bpf_rate.h" +#include "policy_filter.h" + char _license[] __attribute__((section("license"), used)) = "Dual BSD/GPL"; struct { diff --git a/bpf/process/policy_filter.h b/bpf/process/policy_filter.h index 04fd2abbcca..c97af229eab 100644 --- a/bpf/process/policy_filter.h +++ b/bpf/process/policy_filter.h @@ -7,6 +7,14 @@ #include "bpf_tracing.h" #define POLICY_FILTER_MAX_POLICIES 128 +#define POLICY_FILTER_MAX_NAMESPACES 1024 + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, POLICY_FILTER_MAX_NAMESPACES); + __uint(key_size, sizeof(u64)); + __uint(value_size, sizeof(u64)); +} cgroup_namespace_map SEC(".maps"); struct { __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); diff --git a/cmd/tetra/dump/dump.go b/cmd/tetra/dump/dump.go index 94382e3082c..587f65ef897 100644 --- a/cmd/tetra/dump/dump.go +++ b/cmd/tetra/dump/dump.go @@ -128,3 +128,28 @@ func PolicyfilterState(fname string) { fmt.Printf("%d: %s\n", polId, strings.Join(ids, ",")) } } + +func NamespaceState(fname string) error { + m, err := ebpf.LoadPinnedMap(fname, &ebpf.LoadPinOptions{ + ReadOnly: true, + }) + if err != nil { + logger.GetLogger().WithError(err).WithField("file", fname).Warn("Could not open process tree map") + return err + } + + defer m.Close() + + var ( + key uint64 + val uint64 + ) + + fmt.Printf("cgroupId: stableId\n") + iter := m.Iterate() + for iter.Next(&key, &val) { + fmt.Printf("%d: %d\n", key, val) + } + + return nil +} diff --git a/cmd/tetra/policyfilter/policyfilter.go b/cmd/tetra/policyfilter/policyfilter.go index b94045b8704..34ad4e11c61 100644 --- a/cmd/tetra/policyfilter/policyfilter.go +++ b/cmd/tetra/policyfilter/policyfilter.go @@ -28,11 +28,28 @@ func New() *cobra.Command { dumpCmd(), addCommand(), cgroupGetIDCommand(), + dumpDebugCmd(), ) return ret } +func dumpDebugCmd() *cobra.Command { + mapFname := filepath.Join(defaults.DefaultMapRoot, defaults.DefaultMapPrefix, policyfilter.CgrpNsMapName) + ret := &cobra.Command{ + Use: "dumpcgrp", + Short: "dump cgroup ID to namespace state", + Args: cobra.ExactArgs(0), + Run: func(_ *cobra.Command, _ []string) { + dump.NamespaceState(mapFname) + }, + } + + flags := ret.Flags() + flags.StringVar(&mapFname, "map-fname", mapFname, "policyfilter map filename") + return ret +} + func cgroupGetIDCommand() *cobra.Command { mapFname := filepath.Join(defaults.DefaultMapRoot, defaults.DefaultMapPrefix, policyfilter.MapName) ret := &cobra.Command{ diff --git a/pkg/policyfilter/namespace.go b/pkg/policyfilter/namespace.go new file mode 100644 index 00000000000..983f420ae09 --- /dev/null +++ b/pkg/policyfilter/namespace.go @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Tetragon + +package policyfilter + +import ( + "fmt" + "os" + "path" + "path/filepath" + + "github.com/cilium/ebpf" + "github.com/cilium/tetragon/pkg/bpf" + "github.com/cilium/tetragon/pkg/kernels" + "github.com/cilium/tetragon/pkg/logger" + "github.com/cilium/tetragon/pkg/option" + lru "github.com/hashicorp/golang-lru/v2" +) + +const ( + CgrpNsMapName = "cgroup_namespace_map" + namespaceCacheSize = 1024 +) + +// ExecObj returns the exec object based on the kernel version +func execObj() string { + if kernels.EnableV61Progs() { + return "bpf_execve_event_v61.o" + } else if kernels.MinKernelVersion("5.11") { + return "bpf_execve_event_v511.o" + } else if kernels.EnableLargeProgs() { + return "bpf_execve_event_v53.o" + } + return "bpf_execve_event.o" +} + +// NamespaceMap is a simple wrapper for ebpf.Map so that we can write methods for it +type NamespaceMap struct { + cgroupIdMap *ebpf.Map + nsIdMap *lru.Cache[uint64, string] + id uint64 +} + +// newNamespaceMap returns a new namespace mapping. The namespace map consists of +// two pieces. First a cgroup to ID map. The ID is useful for BPF so we can avoid +// strings in BPF side. Then a stable ID to namespace mapping. +func newNamespaceMap() (*NamespaceMap, error) { + cache, err := lru.New[uint64, string](namespaceCacheSize) + if err != nil { + return nil, fmt.Errorf("create namespace ID cache failed") + } + + objName := execObj() + objPath := path.Join(option.Config.HubbleLib, objName) + spec, err := ebpf.LoadCollectionSpec(objPath) + if err != nil { + return nil, fmt.Errorf("loading spec for %s failed: %w", objPath, err) + } + nsMapSpec, ok := spec.Maps[CgrpNsMapName] + if !ok { + return nil, fmt.Errorf("%s not found in %s", CgrpNsMapName, objPath) + } + + ret, err := ebpf.NewMap(nsMapSpec) + if err != nil { + return nil, err + } + + mapDir := bpf.MapPrefixPath() + pinPath := filepath.Join(mapDir, CgrpNsMapName) + os.Remove(pinPath) + os.Mkdir(mapDir, os.ModeDir) + err = ret.Pin(pinPath) + if err != nil { + ret.Close() + return nil, fmt.Errorf("failed to pin Namespace map in %s: %w", pinPath, err) + } + + return &NamespaceMap{ + cgroupIdMap: ret, + nsIdMap: cache, + }, err +} + +// release closes the namespace BPF map, removes (unpin) the bpffs file. +// Then the LRU cache is cleared. +func (m NamespaceMap) release() error { + if err := m.cgroupIdMap.Close(); err != nil { + return err + } + + // nolint:revive // ignore "if-return: redundant if just return error" for clarity + if err := m.cgroupIdMap.Unpin(); err != nil { + return err + } + + m.nsIdMap.Purge() + return nil +} + +func (m NamespaceMap) readBpf() (map[uint64]uint64, error) { + var mapping map[uint64]uint64 + var err error + + file := filepath.Join(bpf.MapPrefixPath(), CgrpNsMapName) + + m.cgroupIdMap, err = ebpf.LoadPinnedMap(file, nil) + if err != nil { + logger.GetLogger().WithError(err).WithField("file", file).Warn("Could not open process tree map") + return mapping, err + } + + defer m.cgroupIdMap.Close() + + var ( + key uint64 + val uint64 + ) + + iter := m.cgroupIdMap.Iterate() + for iter.Next(&key, &val) { + mapping[key] = val + } + + return mapping, nil +} + +func (m NamespaceMap) readNamespace(cgrps map[uint64]uint64) (map[uint64]string, error) { + var mapping map[uint64]string + + for _, k := range cgrps { + v, ok := m.nsIdMap.Get(k) + if ok == false { + logger.GetLogger().WithField("cgrpid", k).Warn("Cgrpid not in namespace mapping") + continue + } + mapping[k] = v + } + return mapping, nil +} + +// addCgroupIDs add cgroups ids to the policy map +// todo: use batch operations when supported +func (m NamespaceMap) addCgroupIDs(cinfo []containerInfo) error { + for _, c := range cinfo { + if err := m.cgroupIdMap.Update(&c.cgID, m.id, ebpf.UpdateAny); err != nil { + logger.GetLogger().WithError(err).WithField("cgid", c.cgID).WithField("id", m.id).WithField("ns", c.name).Warn("Unable to insert cgroup id map") + continue + } + if ok := m.nsIdMap.Add(m.id, c.name); ok != false { + logger.GetLogger().WithField("cgid", c.cgID).WithField("id", m.id).WithField("ns", c.name).Warn("Id to namespace map caused eviction") + } + m.id++ + } + + return nil +} diff --git a/pkg/policyfilter/state.go b/pkg/policyfilter/state.go index f6c24eab6ef..fac7b652e9b 100644 --- a/pkg/policyfilter/state.go +++ b/pkg/policyfilter/state.go @@ -249,6 +249,9 @@ type state struct { // polify filters (outer) map handle pfMap PfMap + // global policy map handle + global *NamespaceMap + cgidFinder cgidFinder } @@ -279,6 +282,11 @@ func newState( return nil, err } + ret.global, err = newNamespaceMap() + if err != nil { + return nil, err + } + return ret, nil } @@ -554,6 +562,11 @@ func (m *state) addPodContainers(pod *podInfo, containerIDs []string, "containers-info": cinfo, }).Info("addPodContainers: container(s) added") + // update global + if m.global != nil { + m.global.addCgroupIDs(cinfo) + } + // update matching policy maps for _, policyID := range pod.matchedPolicies { pol := m.findPolicy(policyID)