From bc7d08d3b7d028704739ae5f89062fe887eb61be Mon Sep 17 00:00:00 2001 From: Roberto Scolaro Date: Sat, 7 Dec 2024 00:07:16 +0000 Subject: [PATCH] feat(libsinsp): add support for containerd interface Signed-off-by: Roberto Scolaro --- test/libsinsp_e2e/container/container.cpp | 2 +- userspace/libsinsp/CMakeLists.txt | 50 ++++ userspace/libsinsp/container.cpp | 6 + .../libsinsp/container_engine/containerd.cpp | 231 ++++++++++++++++++ .../libsinsp/container_engine/containerd.h | 58 +++++ .../containerd/containers.proto | 181 ++++++++++++++ userspace/libsinsp/examples/test.cpp | 2 + userspace/libsinsp/runc.cpp | 33 ++- 8 files changed, 558 insertions(+), 5 deletions(-) create mode 100644 userspace/libsinsp/container_engine/containerd.cpp create mode 100644 userspace/libsinsp/container_engine/containerd.h create mode 100644 userspace/libsinsp/container_engine/containerd/containers.proto diff --git a/test/libsinsp_e2e/container/container.cpp b/test/libsinsp_e2e/container/container.cpp index 522cf02ed9..a51af849f3 100644 --- a/test/libsinsp_e2e/container/container.cpp +++ b/test/libsinsp_e2e/container/container.cpp @@ -393,7 +393,7 @@ TEST_F(sys_call_test, container_docker_bad_socket) { ASSERT_NE(PPME_CONTAINER_JSON_2_E, param.m_evt->get_type()); sinsp_threadinfo* tinfo = param.m_evt->get_thread_info(false); - ASSERT_TRUE(tinfo->m_container_id.length() == 12); + ASSERT_TRUE(tinfo->m_container_id.length() <= 12); ASSERT_TRUE(param.m_inspector->m_container_manager.container_exists(tinfo->m_container_id)); const auto container_info = param.m_inspector->m_container_manager.get_container(tinfo->m_container_id); diff --git a/userspace/libsinsp/CMakeLists.txt b/userspace/libsinsp/CMakeLists.txt index 9a0404f7e2..ab778a659c 100644 --- a/userspace/libsinsp/CMakeLists.txt +++ b/userspace/libsinsp/CMakeLists.txt @@ -128,6 +128,7 @@ if(NOT MINIMAL_BUILD AND NOT EMSCRIPTEN) PRIVATE container_engine/docker/docker_linux.cpp container_engine/docker/connection_linux.cpp container_engine/docker/podman.cpp + container_engine/containerd.cpp container_engine/libvirt_lxc.cpp container_engine/lxc.cpp container_engine/mesos.cpp @@ -243,6 +244,53 @@ function(prepare_cri_grpc api_version) endif() endfunction() +function(prepare_containerd_grpc) + set(DEST ${CMAKE_CURRENT_BINARY_DIR}/container_engine/containerd) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/container_engine/containerd/containers.proto + ${DEST}/containers.proto COPYONLY + ) + add_custom_command( + OUTPUT ${DEST}/containers.grpc.pb.cc ${DEST}/containers.grpc.pb.h ${DEST}/containers.pb.cc + ${DEST}/containers.pb.h + COMMENT "Generate containerd grpc code" + DEPENDS + COMMAND ${PROTOC} -I ${DEST} --cpp_out=${DEST} ${DEST}/containers.proto + COMMAND ${PROTOC} -I ${DEST} --grpc_out=. --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN} + ${DEST}/containers.proto + WORKING_DIRECTORY ${DEST} + ) + add_library(containerd_interface STATIC ${DEST}/containers.pb.cc ${DEST}/containers.grpc.pb.cc) + target_include_directories(containerd_interface PUBLIC $) + target_link_libraries( + containerd_interface + PUBLIC "${GRPCPP_LIB}" + "${GRPC_LIB}" + "${GPR_LIB}" + "${GRPC_LIBRARIES}" + "${PROTOBUF_LIB}" + "${CARES_LIB}" + "${OPENSSL_LIBRARIES}" + ) + add_dependencies(containerd_interface grpc) + install( + FILES ${DEST}/containers.grpc.pb.h ${DEST}/containers.pb.h + DESTINATION + "${CMAKE_INSTALL_INCLUDEDIR}/${LIBS_PACKAGE_NAME}/libsinsp/container_engine/containerd" + COMPONENT "scap" + ) + if(NOT BUILD_SHARED_LIBS) + install( + TARGETS containerd_interface + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT "scap" + OPTIONAL + ) + endif() +endfunction() + if(NOT EMSCRIPTEN) add_dependencies(sinsp tbb) endif() @@ -260,8 +308,10 @@ if(NOT WIN32) include(cares) prepare_cri_grpc(v1alpha2) prepare_cri_grpc(v1) + prepare_containerd_grpc() target_link_libraries(sinsp PUBLIC cri_v1alpha2 cri_v1) + target_link_libraries(sinsp PUBLIC containerd_interface) if(NOT MUSL_OPTIMIZED_BUILD) find_library(LIB_ANL anl) diff --git a/userspace/libsinsp/container.cpp b/userspace/libsinsp/container.cpp index f6f74a3c09..ae4af75e70 100644 --- a/userspace/libsinsp/container.cpp +++ b/userspace/libsinsp/container.cpp @@ -30,6 +30,7 @@ limitations under the License. #include #include #include +#include #endif // MINIMAL_BUILD #include @@ -599,6 +600,11 @@ void sinsp_container_manager::create_engines() { m_container_engines.push_back(bpm_engine); m_container_engine_by_type[CT_BPM] = bpm_engine; } + if(m_container_engine_mask & (1 << CT_CONTAINERD)) { + auto containerd_engine = std::make_shared(*this); + m_container_engines.push_back(containerd_engine); + // m_container_engine_by_type[CT_CONTAINERD] = containerd_engine; + } #endif // _WIN32 #endif // MINIMAL_BUILD } diff --git a/userspace/libsinsp/container_engine/containerd.cpp b/userspace/libsinsp/container_engine/containerd.cpp new file mode 100644 index 0000000000..9560df3698 --- /dev/null +++ b/userspace/libsinsp/container_engine/containerd.cpp @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2024 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +#include + +#include +#include +#include +#include +#include + +using namespace libsinsp::container_engine; +using namespace libsinsp::runc; + +constexpr const cgroup_layout CONTAINERD_CGROUP_LAYOUT[] = {{"/default/", ""}, {nullptr, nullptr}}; + +constexpr const std::string_view CONTAINERD_SOCKETS[] = { + "/run/host-containerd/containerd.sock", // bottlerocket host containers socket + "/run/containerd/runtime2/containerd.sock", // tmp +}; + +bool containerd_interface::is_ok() { + return m_stub != nullptr; +} +containerd_interface::containerd_interface(const std::string &socket_path) { + grpc::ChannelArguments args; + args.SetInt(GRPC_ARG_ENABLE_HTTP_PROXY, 0); + std::shared_ptr channel = + libsinsp::grpc_channel_registry::get_channel("unix://" + socket_path, &args); + + m_stub = ContainerdService::Containers::NewStub(channel); + + ContainerdService::ListContainersRequest req; + ContainerdService::ListContainersResponse resp; + + grpc::ClientContext context; + auto deadline = std::chrono::system_clock::now() + + std::chrono::milliseconds(libsinsp::cri::cri_settings::get_cri_timeout()); + context.set_deadline(deadline); + + // The `default` namesapce is the default one of containerd + // and the one used by host-containers in bottlerocket. + // This is mandatory to query the containers. + context.AddMetadata("containerd-namespace", "default"); + grpc::Status status = m_stub->List(&context, req, &resp); + + if(!status.ok()) { + libsinsp_logger()->format(sinsp_logger::SEV_NOTICE, + "containerd (%s): containerd runtime returned an error after " + "trying to list containerd: %s", + socket_path.c_str(), + status.error_message().c_str()); + m_stub.reset(nullptr); + return; + } +} + +grpc::Status containerd_interface::list_container_resp( + const std::string &container_id, + ContainerdService::ListContainersResponse &resp) { + ContainerdService::ListContainersRequest req; + + // To match the container using a truncated containerd id + // we need to use a match filter (~=). + req.add_filters("id~=" + container_id); + grpc::ClientContext context; + context.AddMetadata("containerd-namespace", "default"); + auto deadline = std::chrono::system_clock::now() + + std::chrono::milliseconds(libsinsp::cri::cri_settings::get_cri_timeout()); + context.set_deadline(deadline); + return m_stub->List(&context, req, &resp); +} + +libsinsp::container_engine::containerd::containerd(container_cache_interface &cache): + container_engine_base(cache) { + for(const auto &p : CONTAINERD_SOCKETS) { + if(p.empty()) { + continue; + } + + auto socket_path = scap_get_host_root() + std::string(p); + struct stat s = {}; + if(stat(socket_path.c_str(), &s) != 0 || (s.st_mode & S_IFMT) != S_IFSOCK) { + continue; + } + + m_interface = std::make_unique(socket_path); + if(!m_interface->is_ok()) { + m_interface.reset(nullptr); + continue; + } + } +} + +bool libsinsp::container_engine::containerd::parse_containerd(sinsp_container_info &container, + const std::string &container_id) { + // given the truncated container id, the full container id needs to be retrivied from + // containerd. + ContainerdService::ListContainersResponse resp; + grpc::Status status = m_interface->list_container_resp(container_id, resp); + + if(!status.ok()) { + libsinsp_logger()->format( + sinsp_logger::SEV_DEBUG, + "containerd (%s): ListContainerResponse status error message: (%s)", + container.m_id.c_str(), + status.error_message().c_str()); + return false; + } + + auto containers = resp.containers(); + + if(containers.size() == 0) { + libsinsp_logger()->format(sinsp_logger::SEV_DEBUG, + "containerd (%s): ListContainerResponse status error message: " + "(container id has no match)", + container.m_id.c_str()); + return false; + } else if(containers.size() > 1) { + libsinsp_logger()->format(sinsp_logger::SEV_DEBUG, + "containerd (%s): ListContainerResponse status error message: " + "(container id has more than one match)", + container.m_id.c_str()); + return false; + } + + // Usually the image has this form: `docker.io/library/ubuntu:22.04` + auto raw_image_splits = sinsp_split(containers[0].image(), ':'); + + container.m_id = container_id; + container.m_full_id = containers[0].id(); + // We assume that the last `/`-separated field is the image + container.m_image = raw_image_splits[0].substr(raw_image_splits[0].rfind("/") + 1); + // and the first part is the repo + container.m_imagerepo = raw_image_splits[0].substr(0, raw_image_splits[0].rfind("/")); + container.m_imagetag = raw_image_splits[1]; + container.m_imagedigest = ""; + container.m_type = CT_CONTAINERD; + + // Retrieve the labels. + for(const auto &pair : containers[0].labels()) { + if(pair.second.length() <= sinsp_container_info::m_container_label_max_length) { + container.m_labels[pair.first] = pair.second; + } + } + + // The spec field keeps the information about the mounts. + Json::Value spec; + Json::Reader reader; + // The spec field of the response is just a raw json. + reader.parse(containers[0].spec().value(), spec); + + // Retrieve the mounts. + for(const auto &m : spec["mounts"]) { + bool readonly = false; + std::string mode; + for(const auto &jopt : m["options"]) { + std::string opt = jopt.asString(); + if(opt == "ro") { + readonly = true; + } else if(opt.rfind("mode=") == 0) { + mode = opt.substr(5); + } + } + container.m_mounts.emplace_back(m["source"].asString(), + m["destination"].asString(), + mode, + !readonly, + spec["linux"]["rootfsPropagation"].asString()); + } + + // Retrieve the env. + for(const auto &env : spec["process"]["env"]) { + container.m_env.emplace_back(env.asString()); + } + + return true; +} + +bool libsinsp::container_engine::containerd::resolve(sinsp_threadinfo *tinfo, + bool query_os_for_missing_info) { + auto container = sinsp_container_info(); + std::string container_id, cgroup; + + if(!matches_runc_cgroups(tinfo, CONTAINERD_CGROUP_LAYOUT, container_id, cgroup)) { + return false; + } + + if(!parse_containerd(container, container_id)) { + return false; + } + + tinfo->m_container_id = container_id; + + libsinsp::cgroup_limits::cgroup_limits_key key(container.m_id, + tinfo->get_cgroup("cpu"), + tinfo->get_cgroup("memory"), + tinfo->get_cgroup("cpuset")); + + libsinsp::cgroup_limits::cgroup_limits_value limits; + libsinsp::cgroup_limits::get_cgroup_resource_limits(key, limits); + + container.m_memory_limit = limits.m_memory_limit; + container.m_cpu_shares = limits.m_cpu_shares; + container.m_cpu_quota = limits.m_cpu_quota; + container.m_cpu_period = limits.m_cpu_period; + container.m_cpuset_cpu_count = limits.m_cpuset_cpu_count; + + if(container_cache().should_lookup(container.m_id, CT_CONTAINERD)) { + container.m_name = container.m_id; + container.set_lookup_status(sinsp_container_lookup::state::SUCCESSFUL); + container_cache().add_container(std::make_shared(container), tinfo); + container_cache().notify_new_container(container, tinfo); + } + return true; +} diff --git a/userspace/libsinsp/container_engine/containerd.h b/userspace/libsinsp/container_engine/containerd.h new file mode 100644 index 0000000000..c2def0cc55 --- /dev/null +++ b/userspace/libsinsp/container_engine/containerd.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +/* +Copyright (C) 2024 The Falco Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +#pragma once + +class sinsp_container_info; +class sinsp_threadinfo; + +#include +#include +#include + +namespace ContainerdService = containerd::services::containers::v1; + +namespace libsinsp { +namespace container_engine { + +class containerd_interface { +public: + containerd_interface(const std::string &socket_path); + + grpc::Status list_container_resp(const std::string &container_id, + ContainerdService::ListContainersResponse &resp); + + bool is_ok(); + +private: + std::unique_ptr m_stub; +}; + +class containerd : public container_engine_base { +public: + containerd(container_cache_interface &cache); + + bool parse_containerd(sinsp_container_info &container, const std::string &container_id); + bool resolve(sinsp_threadinfo *tinfo, bool query_os_for_missing_info) override; + +private: + std::unique_ptr m_interface; +}; + +} // namespace container_engine +} // namespace libsinsp diff --git a/userspace/libsinsp/container_engine/containerd/containers.proto b/userspace/libsinsp/container_engine/containerd/containers.proto new file mode 100644 index 0000000000..3de07ffbd6 --- /dev/null +++ b/userspace/libsinsp/container_engine/containerd/containers.proto @@ -0,0 +1,181 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +syntax = "proto3"; + +package containerd.services.containers.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/containerd/containerd/api/services/containers/v1;containers"; + +// Containers provides metadata storage for containers used in the execution +// service. +// +// The objects here provide an state-independent view of containers for use in +// management and resource pinning. From that perspective, containers do not +// have a "state" but rather this is the set of resources that will be +// considered in use by the container. +// +// From the perspective of the execution service, these objects represent the +// base parameters for creating a container process. +// +// In general, when looking to add fields for this type, first ask yourself +// whether or not the function of the field has to do with runtime execution or +// is invariant of the runtime state of the container. If it has to do with +// runtime, or changes as the "container" is started and stops, it probably +// doesn't belong on this object. +service Containers { + rpc Get(GetContainerRequest) returns (GetContainerResponse); + rpc List(ListContainersRequest) returns (ListContainersResponse); + rpc ListStream(ListContainersRequest) returns (stream ListContainerMessage); + rpc Create(CreateContainerRequest) returns (CreateContainerResponse); + rpc Update(UpdateContainerRequest) returns (UpdateContainerResponse); + rpc Delete(DeleteContainerRequest) returns (google.protobuf.Empty); +} + +message Container { + // ID is the user-specified identifier. + // + // This field may not be updated. + string id = 1; + + // Labels provides an area to include arbitrary data on containers. + // + // The combined size of a key/value pair cannot exceed 4096 bytes. + // + // Note that to add a new value to this field, read the existing set and + // include the entire result in the update call. + map labels = 2; + + // Image contains the reference of the image used to build the + // specification and snapshots for running this container. + // + // If this field is updated, the spec and rootfs needed to updated, as well. + string image = 3; + + message Runtime { + // Name is the name of the runtime. + string name = 1; + // Options specify additional runtime initialization options. + google.protobuf.Any options = 2; + } + // Runtime specifies which runtime to use for executing this container. + Runtime runtime = 4; + + // Spec to be used when creating the container. This is runtime specific. + google.protobuf.Any spec = 5; + + // Snapshotter specifies the snapshotter name used for rootfs + string snapshotter = 6; + + // SnapshotKey specifies the snapshot key to use for the container's root + // filesystem. When starting a task from this container, a caller should + // look up the mounts from the snapshot service and include those on the + // task create request. + // + // Snapshots referenced in this field will not be garbage collected. + // + // This field is set to empty when the rootfs is not a snapshot. + // + // This field may be updated. + string snapshot_key = 7; + + // CreatedAt is the time the container was first created. + google.protobuf.Timestamp created_at = 8; + + // UpdatedAt is the last time the container was mutated. + google.protobuf.Timestamp updated_at = 9; + + // Extensions allow clients to provide zero or more blobs that are directly + // associated with the container. One may provide protobuf, json, or other + // encoding formats. The primary use of this is to further decorate the + // container object with fields that may be specific to a client integration. + // + // The key portion of this map should identify a "name" for the extension + // that should be unique against other extensions. When updating extension + // data, one should only update the specified extension using field paths + // to select a specific map key. + map extensions = 10; + + // Sandbox ID this container belongs to. + string sandbox = 11; +} + +message GetContainerRequest { + string id = 1; +} + +message GetContainerResponse { + Container container = 1; +} + +message ListContainersRequest { + // Filters contains one or more filters using the syntax defined in the + // containerd filter package. + // + // The returned result will be those that match any of the provided + // filters. Expanded, containers that match the following will be + // returned: + // + // filters[0] or filters[1] or ... or filters[n-1] or filters[n] + // + // If filters is zero-length or nil, all items will be returned. + repeated string filters = 1; +} + +message ListContainersResponse { + repeated Container containers = 1; +} + +message CreateContainerRequest { + Container container = 1; +} + +message CreateContainerResponse { + Container container = 1; +} + +// UpdateContainerRequest updates the metadata on one or more container. +// +// The operation should follow semantics described in +// https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/field-mask, +// unless otherwise qualified. +message UpdateContainerRequest { + // Container provides the target values, as declared by the mask, for the update. + // + // The ID field must be set. + Container container = 1; + + // UpdateMask specifies which fields to perform the update on. If empty, + // the operation applies to all fields. + google.protobuf.FieldMask update_mask = 2; +} + +message UpdateContainerResponse { + Container container = 1; +} + +message DeleteContainerRequest { + string id = 1; +} + +message ListContainerMessage { + Container container = 1; +} diff --git a/userspace/libsinsp/examples/test.cpp b/userspace/libsinsp/examples/test.cpp index 3d56d72522..679b53af22 100644 --- a/userspace/libsinsp/examples/test.cpp +++ b/userspace/libsinsp/examples/test.cpp @@ -470,6 +470,8 @@ int main(int argc, char** argv) { } } + inspector.set_cri_socket_path(""); + auto events_sc_codes = extract_filter_sc_codes(inspector); if(!events_sc_codes.empty()) { auto events_sc_names = libsinsp::events::sc_set_to_sc_names(events_sc_codes); diff --git a/userspace/libsinsp/runc.cpp b/userspace/libsinsp/runc.cpp index 178dea09e2..169efe2230 100644 --- a/userspace/libsinsp/runc.cpp +++ b/userspace/libsinsp/runc.cpp @@ -27,7 +27,6 @@ namespace { const size_t CONTAINER_ID_LENGTH = 64; const size_t REPORTED_CONTAINER_ID_LENGTH = 12; -const char *CONTAINER_ID_VALID_CHARACTERS = "0123456789abcdefABCDEF"; static_assert(REPORTED_CONTAINER_ID_LENGTH <= CONTAINER_ID_LENGTH, "Reported container ID length cannot be longer than actual length"); @@ -37,6 +36,30 @@ static_assert(REPORTED_CONTAINER_ID_LENGTH <= CONTAINER_ID_LENGTH, namespace libsinsp { namespace runc { +inline static bool is_host(const std::string &cgroup, const std::string &container_id) { + size_t start_pos = cgroup.rfind("/user.slice"); + if(start_pos != std::string::npos) { + return container_id.rfind("session-") == 0 || container_id == "user.slice"; + } + + start_pos = cgroup.rfind("/system.slice"); + if(start_pos != std::string::npos) { + return true; + } + + start_pos = cgroup.rfind("/-.slice"); + if(start_pos != std::string::npos) { + return true; + } + + start_pos = cgroup.rfind("/init.scope"); + if(start_pos != std::string::npos) { + return true; + } + + return false; +} + // check if cgroup ends with // If true, set to a truncated version of the id and return true. // Otherwise return false and leave container_id unchanged @@ -55,12 +78,14 @@ bool match_one_container_id(const std::string &cgroup, return false; } - if(end_pos - start_pos != CONTAINER_ID_LENGTH) { + // In some container runtimes the container the container id is not + // necessarly CONTAINER_ID_LENGTH long and can be arbitrarly defined. + // To keep it simple we only discard the container id > of CONTAINER_ID_LENGTH. + if(end_pos - start_pos > CONTAINER_ID_LENGTH || end_pos - start_pos == 0) { return false; } - size_t invalid_ch_pos = cgroup.find_first_not_of(CONTAINER_ID_VALID_CHARACTERS, start_pos); - if(invalid_ch_pos < CONTAINER_ID_LENGTH) { + if(is_host(cgroup, cgroup.substr(start_pos))) { return false; }