https://h0x0er.github.io/blog/2025/06/29/ebpf-connecting-with-container-runtimes/ [ ] [ ] Skip to content Unnamed Memories eBPF: Connecting with Container Runtimes ( ) ( ) ( ) * * Logs * Debug * About Unnamed Memories * * [*] Logs Logs + [ ] Archive Archive o 2025 + [ ] Categories Categories o ebpf * [ ] Debug Debug + eBPF + Kubernetes * [ ] About About + Work Experiences + Achievements Table of contents * Objective * Reasoning + Locate unix-socket file o Tetragon o Crictl o Tracee + Making connection o Tetragon o Crictl o Tracee + Query the info o Tetragon o Tracee * Refer Back to index Jatin Jatin * Metadata + Sunday, June 29, 2025 + Thursday, July 10, 2025 + in ebpf + 4 min read eBPF: Connecting with Container Runtimes Objective * to understand how connection with Container Runtime (CR) is being made using Container Runtime Interface (CRI) in different open-source eBPF-based projects. + to query pod or container info for context enrichment. --------------------------------------------------------------------- Reasoning Note Code snippets are take from open-source tetragon, tracee and crictl projects. Connection with CR is important for making the tool/product kubernetes-aware. As it provides rich information that could be of interest for different use-cases. Connection with CR involves following steps * locate unix-socket file * make a grpc connection using CRI API * query the info Locate unix-socket file Tip Make sure to mount host /var or /run in container. Most of the times these are in a well-known location such as /var/run or /run. Checkout CR documentation for exact location. In projects that I explored, well-known paths are hardcoded for flexibility. During runtime, code iterate over these paths, tries to make a connection and returns the corresponding service, if it was success. Tetragon Tetragon contains some hardcoded default sock-paths. [Source] 1 defaultEndpoints = []string{ 2 "unix:///run/containerd/containerd.sock", 3 "unix:///run/crio/crio.sock", 4 "unix:///var/run/cri-dockerd.sock", 5 } Crictl Browse full source-code 1 var defaultRuntimeEndpoints = []string{"unix:///run/containerd/containerd.sock", "unix:///run/crio/crio.sock", "unix:///var/run/cri-dockerd.sock"} Tracee Browse full source-code 1 func Autodiscover(onRegisterFail func(err error, runtime RuntimeId, socket string)) Sockets { 2 register := func(sockets *Sockets, runtime RuntimeId, socket string) { 3 err := sockets.Register(runtime, socket) 4 if err != nil { 5 onRegisterFail(err, runtime, socket) 6 } 7 } 8 sockets := Sockets{} 9 const ( 10 defaultContainerd = "/var/run/containerd/containerd.sock" 11 defaultDocker = "/var/run/docker.sock" 12 defaultCrio = "/var/run/crio/crio.sock" 13 defaultPodman = "/var/run/podman/podman.sock" 14 ) 15 16 register(&sockets, Containerd, defaultContainerd) 17 register(&sockets, Docker, defaultDocker) 18 register(&sockets, Crio, defaultCrio) 19 register(&sockets, Podman, defaultPodman) 20 21 return sockets 22 } Making connection Tetragon Browse full source-code 1 // required modules 2 import ( 3 "google.golang.org/grpc" 4 "google.golang.org/grpc/credentials/insecure" 5 criapi "k8s.io/cri-api/pkg/apis/runtime/v1" 6 ) 7 8 func newClientTry(ctx context.Context, endpoint string) (criapi.RuntimeServiceClient, error) { 9 10 u, err := url.Parse(endpoint) 11 if err != nil { 12 return nil, err 13 } 14 if u.Scheme != "unix" { 15 return nil, errNotUnix 16 } 17 18 conn, err := grpc.NewClient(endpoint, 19 grpc.WithTransportCredentials(insecure.NewCredentials()), 20 ) 21 if err != nil { 22 return nil, err 23 } 24 25 rtcli := criapi.NewRuntimeServiceClient(conn) 26 if _, err := rtcli.Version(ctx, &criapi.VersionRequest{}); err != nil { 27 return nil, fmt.Errorf("validate CRI v1 runtime API for endpoint %q: %w", endpoint, err) 28 } 29 30 return rtcli, nil 31 } Crictl Browse full source-code 1 // required modules 2 import( 3 ... 4 internalapi "k8s.io/cri-api/pkg/apis" 5 remote "k8s.io/cri-client/pkg" 6 ... 7 ) 8 9 ... 10 for _, endPoint := range defaultRuntimeEndpoints { 11 logrus.Debugf("Connect using endpoint %q with %q timeout", endPoint, t) 12 13 res, err = remote.NewRemoteRuntimeService(endPoint, t, tp, &logger) 14 if err != nil { 15 logrus.Error(err) 16 17 continue 18 } 19 20 logrus.Debugf("Connected successfully using endpoint: %s", endPoint) 21 22 break 23 } 24 ... Tracee Browse full source-code 1 func ContainerdEnricher(socket string) (ContainerEnricher, error) { 2 enricher := containerdEnricher{} 3 4 // avoid duplicate unix:// prefix 5 unixSocket := "unix://" + strings.TrimPrefix(socket, "unix://") 6 7 client, err := containerd.New(socket) 8 if err != nil { 9 return nil, errfmt.WrapError(err) 10 } 11 12 conn, err := grpc.NewClient(unixSocket, grpc.WithTransportCredentials(insecure.NewCredentials())) 13 if err != nil { 14 if errC := client.Close(); errC != nil { 15 logger.Errorw("Closing containerd connection", "error", errC) 16 } 17 return nil, errfmt.WrapError(err) 18 } 19 20 enricher.images_cri = cri.NewImageServiceClient(conn) 21 enricher.containers = client.ContainerService() 22 enricher.namespaces = client.NamespaceService() 23 enricher.images = client.ImageService() 24 25 return &enricher, nil 26 } Query the info Tetragon Querying cgroup-path of a container. [Source] 1 func CgroupPath(ctx context.Context, cli criapi.RuntimeServiceClient, containerID string) (string, error) { 2 3 // creating a request 4 req := criapi.ContainerStatusRequest{ 5 ContainerId: containerID, 6 Verbose: true, 7 } 8 9 // making grpc call 10 res, err := cli.ContainerStatus(ctx, &req) 11 if err != nil { 12 return "", err 13 } 14 15 // taking the info 16 info := res.GetInfo() 17 if info == nil { 18 return "", errors.New("no container info") 19 } 20 21 // extracting the relevant info 22 23 var path, json string 24 if infoJson, ok := info["info"]; ok { 25 json = infoJson 26 path = "runtimeSpec.linux.cgroupsPath" 27 } else { 28 return "", errors.New("could not find info") 29 } 30 31 ret := gjson.Get(json, path).String() 32 if ret == "" { 33 return "", errors.New("failed to find cgroupsPath in json") 34 } 35 36 return ParseCgroupsPath(ret) 37 } --------------------------------------------------------------------- Tracee Browse full source-code tracee-snippet.go 1 func (e *containerdEnricher) Get(ctx context.Context, containerId string) (EnrichResult, error) { 2 res := EnrichResult{} 3 nsList, err := e.namespaces.List(ctx) 4 if err != nil { 5 return res, errfmt.Errorf("failed to fetch namespaces %s", err.Error()) 6 } 7 for _, namespace := range nsList { 8 // always query with namespace applied 9 nsCtx := namespaces.WithNamespace(ctx, namespace) 10 11 // if containers is not in current namespace, search the next one 12 container, err := e.containers.Get(nsCtx, containerId) 13 if err != nil { 14 continue 15 } 16 17 .... 18 19 // if in k8s we can extract pod info from labels 20 if container.Labels != nil { 21 labels := container.Labels 22 res.PodName = labels[PodNameLabel] 23 res.Namespace = labels[PodNamespaceLabel] 24 res.UID = labels[PodUIDLabel] 25 res.Sandbox = e.isSandbox(labels) 26 27 // containerd containers normally have no names unless set from k8s 28 res.ContName = labels[ContainerNameLabel] 29 } 30 res.Image = imageName 31 res.ImageDigest = imageDigest 32 33 return res, nil 34 } 35 36 return res, errfmt.Errorf("failed to find container %s in any namespace", containerId) 37 } --------------------------------------------------------------------- Refer * https://github.com/cilium/tetragon/blob/main/pkg/cri/cri.go * https://github.com/cilium/tetragon/tree/main/pkg/cri * https://github.com/kubernetes-sigs/cri-tools/blob/ 0cf370b13928d79146916fd9accbbc69f64a92b5/cmd/crictl/main.go#L73 * https://github.com/aquasecurity/tracee/tree/main/pkg/containers/ runtime Back to top Copyright (c) 2025 JATIN KUMAR Made with Material for MkDocs