Replace aquasecurity/libbpfgo with cilium/ebpf (#60541)

* start the refactor away from aquasecurity/libbpfgo to use cilium/ebpf

Remove debug statements and unused code in BPF library

This commit eliminates unnecessary debug log statements from bpf_test.go and removes unused code in disk.go and network.go files within BPF library. The aim is to make codebase cleaner, enhancing readability and maintainability without losing track of functional aspects. It's part of an ongoing effort to maintain optimal code quality across the project.

Revise Dockerfile and Makefile instructions

Adjusted Dockerfile and Makefile commands to optimize build process. The changes ensure better readability and maintainability of code, especially with respect to BPF bytecode and test preparations. A clause was also added to check for the presence of clang before building BPF bytecode.

Refactor BPF code and tests

Refactored BPF source code and its corresponding tests for better readability and efficiency. This includes updating data structures like rawExecEvent, rawOpenEvent, rawConn4Event and rawConn6Event to their more meritorious versions like commandDataT, diskDataT, networkIpv4DataT and networkIpv6DataT respectively. Also, generated concise and clear bindigs for eBPF programs to replace the previous multi-arch setup. Furthermore, correspondingly adapted all related test cases arising in bpf_test.go, command.go and other files.

Remove counter_test from BPF and related test code

This commit removes the file counter_test.bpf.c from the enhanced recording in BPF. Following this file deletion, corresponding test functions in bpf_test.go, that were originally testing the functionality provided by counter_test.bpf.c, have been removed as well.

Refactor BPF test and improve error handling

The commit refactors the BPF test suite to run each test as a sub-test by using t.Run, easing debugging and error tracing. It also simplifies the 'executeCommand' function by removing redundant function calls related to finding executable files, and improves error handling. In 'cgroup.go', the code has been adjusted to retry on EBUSY errors when removing a cgroup, instead of ignoring them.

Update cgroup deletion process and error handling

The changes made introduce a retry mechanism when deleting a cgroup in case the cgroup is busy. This prevents a failed attempt from immediately returning an error. Additionally, error handling has been improved in the 'writePids' function to better handle 'no such process' errors.

Enhance cgroup monitoring in disk.bpf.c

The code has been updated to check if current cgroup is being monitored in the "enter_open" method of "disk.bpf.c". If the cgroup is not marked for monitoring, it will be ignored, enhancing overall cgroup management.

Improve resource handling

This commit introduces a deferred close to the sendEvents function to ensure proper resource handling of the eventBuf object.

Add rlimit package to remove resource limits

Introduced the "github.com/cilium/ebpf/rlimit" package and added a function to remove resource limits for kernels less than 5.11. This operation is necessary for kernels with a lower version because they may face issues when running applications with allocated memory limitations.

Fix a test after rebase

Refactor BPF testing setup and implement session closing

This commit updates the BPF testing structure to remove the context dependency in event generation functions, simplifying the test setup. Additionally, it adds a 'closed' field to the 'open', 'conn', and 'exec' structs alongside mutex protection. This implementation allows checking if a session is closed before any operations, correctly handling already closed sessions. Furthermore, it corrects tracepoints for handling 'openat' and 'openat2' system calls for different platforms. This refactoring effort aims to improve the robustness and readability of the BPF testing suite.

Update conditions for BPF bytecode building

This update modifies the Makefile to add a conditional check for the presence of /usr/include/bpf/bpf_helpers.h. This new requirement must be met along with the existence of /usr/include/linux/bpf.h and the installation of Clang, in order to facilitate the correct build of BPF bytecode. Consequently, this change enhances the reliability of the build process.

Add conditional for bpf_helpers.h in Makefile

This commit adds an additional check in the Makefile for the existence of /usr/include/bpf/bpf_helpers.h. This ensures that BPF bytecode only gets built when both bpf.h and bpf_helpers.h exist, and Clang is installed, improving the build process reliability.

Refactor and improve BPF bytecode generation

This commit refactors and improves the BPF bytecode generation process. It introduces distinct bytecode structures for different architectures, ensuring better compatibility and execution. The BPF test is adjusted due to its tendency to fail unpredictably, hence it's commented out to avoid unnecessary build failures. This commit also eliminates certain redundancies in `common.mk`, improving the clarity of the build process.

Comment out test code in bpf_test.go

The test code in the file bpf_test.go has been commented out. These changes are circumstantial and do not imply that the test code is obsolete or unnecessary. The use of such tests may be needed in future commits or for local development testing and debugging.

Remove libbpf dependency from Vagrantfile and update README

The Vagrantfile and README have been updated to remove the no longer needed libbpf dependency. This change simplifies the project set up by eliminating unused build steps in the Vagrantfile and correcting related instructions in the README. This results in a more streamlined, intuitive build process and reduced potential for code complexity and dependency conflicts.

Remove obsolete dependencies and related code blocks

The 'aquasecurity/libbpfgo' library referencing and the related code blocks have been removed from the test files and core modules. The corresponding setup instructions in the dockerfiles and build scripts have also been eliminated. This update simplifies the build process and overall project structure while reducing unnecessary dependency risks.

Remove aquasecurity/libbpfgo dependency

Remove restricted.bpf.c

Migrate disk events to cilium

Migrate network events to cilium

Remove restricted session

Use cilium/ebpf for BPF

* fix build issues caused by stale branch

Also updated bpf bytecode, updated github.com/cilium/ebpf to 0.19.0,
and updated bpf/README to reflect changes

* fix tests and a bit of cleanup

Also added a subtest and fixed receiving IPv6 network events and
fixed attempting to create tracepoints for syscalls that are not
present on arm64.

* repurpose buffer size configs to set event channel buffer size

* add new GHA job to check that generated eBPF files are up to date

* fix lost event promethus counters and address feedback

* address feedback

* use only unsafe instead of CGO to convert C strings

* rename common_data_t comm -> command

* convert strings in bpf programs from char arrays to u8 or uint8 arrays so we can convert them to Go strings without the use of the unsafe package

---------

Co-authored-by: Jakub Nyckowski <jakub.nyckowski@goteleport.com>
Co-authored-by: Tim Ross <tim.ross@goteleport.com>
This commit is contained in:
Andrew LeFevre
2025-11-11 10:33:32 -05:00
committed by GitHub
parent e4cf75a29a
commit 7a7f08cb39
50 changed files with 2181 additions and 1246 deletions
-2
View File
@@ -7,8 +7,6 @@ updates:
day: 'sunday'
time: '09:00' # 9am UTC
ignore:
# Must be kept in-sync with libbpf
- dependency-name: github.com/aquasecurity/libbpfgo
# Forked/replaced dependencies
- dependency-name: github.com/alecthomas/kingpin/v2
- dependency-name: github.com/go-mysql-org/go-mysql
-1
View File
@@ -27,7 +27,6 @@ jobs:
TELEPORT_ETCD_TEST: yes
TELEPORT_ETCD_TEST_ENDPOINT: https://etcd0:2379
TELEPORT_XAUTH_TEST: yes
TELEPORT_BPF_TEST: yes
WEBASSETS_SKIP_BUILD: 1
options: --cap-add=SYS_ADMIN --privileged
+26 -1
View File
@@ -17,6 +17,7 @@ jobs:
has_proto: ${{ steps.changes.outputs.has_proto }}
has_rfd: ${{ steps.changes.outputs.has_rfd }}
has_ui: ${{ steps.changes.outputs.has_ui }}
has_bpf: ${{ steps.changes.outputs.has_bpf }}
steps:
- name: Checkout
if: ${{ github.event_name == 'merge_group' }}
@@ -79,6 +80,11 @@ jobs:
- 'tsconfig.json'
- 'tsconfig.node.json'
- 'docs/pages/reference/audit-events.mdx'
has_bpf:
- '.github/workflows/lint.yaml'
- 'bpf/**.c'
- 'bpf/**.h'
- 'lib/bpf/*'
lint-go:
name: Lint (Go)
@@ -132,7 +138,7 @@ jobs:
# If the config is not valid, golangci-lint will fail.
verify: false
version: ${{ env.GOLANGCI_LINT_VERSION }}
args: --build-tags libfido2,piv
args: --build-tags bpf,libfido2,piv
skip-cache: true
- name: golangci-lint (assets/backport)
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
@@ -338,3 +344,22 @@ jobs:
# We have to add the current directory as a safe directory or else git commands will not work as expected.
run: git config --global --add safe.directory $(realpath .) && make audit-event-reference-up-to-date
lint-bpf:
name: BPF Generated Files
needs: changes
if: ${{ !startsWith(github.head_ref, 'dependabot/') && needs.changes.outputs.has_bpf == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
container:
image: ghcr.io/gravitational/teleport-buildbox:teleport19
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check if BPF generated files are up to date
# We have to add the current directory as a safe directory or else git commands will not work as expected.
run: git config --global --add safe.directory $(realpath .) && make bpf-up-to-date
-1
View File
@@ -41,7 +41,6 @@ jobs:
TELEPORT_ETCD_TEST: yes
TELEPORT_ETCD_TEST_ENDPOINT: https://etcd0:2379
TELEPORT_XAUTH_TEST: yes
TELEPORT_BPF_TEST: yes
WEBASSETS_SKIP_BUILD: 1
options: --cap-add=SYS_ADMIN --privileged
+2
View File
@@ -27,6 +27,8 @@ build
!/web/packages/build
/web/packages/shared/libs/ironrdp/pkg/**
*.o
# Do not ignore BPF bytecode files
!/lib/bpf/*.o
*.a
*.so
*.syso
+24 -26
View File
@@ -388,7 +388,7 @@ $(BUILDDIR)/tctl:
GOOS=$(OS) GOARCH=$(ARCH) $(CGOFLAG) go build -tags "$(PAM_TAG) $(FIPS_TAG) $(LIBFIDO2_BUILD_TAG) $(TOUCHID_TAG) $(PIV_BUILD_TAG) $(KUSTOMIZE_NO_DYNAMIC_PLUGIN)" -o $(BUILDDIR)/tctl $(BUILDFLAGS) $(TOOLS_LDFLAGS) ./tool/tctl
.PHONY: $(BUILDDIR)/teleport
$(BUILDDIR)/teleport: ensure-webassets bpf-bytecode rdpclient
$(BUILDDIR)/teleport: ensure-webassets rdpclient
GOOS=$(OS) GOARCH=$(ARCH) $(CGOFLAG) go build -tags "webassets_embed $(PAM_TAG) $(FIPS_TAG) $(BPF_TAG) $(WEBASSETS_TAG) $(RDPCLIENT_TAG) $(PIV_BUILD_TAG) $(KUSTOMIZE_NO_DYNAMIC_PLUGIN)" -o $(BUILDDIR)/teleport $(BUILDFLAGS) $(TELEPORT_LDFLAGS) ./tool/teleport
# NOTE: Any changes to the `tsh` build here must be copied to `build.assets/windows/build.ps1`
@@ -455,33 +455,35 @@ tctl-app:
#
# BPF support (IF ENABLED)
# Requires a recent version of clang and libbpf installed.
# Requires clang 14+
#
ifeq ("$(with_bpf)","yes")
$(ER_BPF_BUILDDIR):
mkdir -p $(ER_BPF_BUILDDIR)
# Build BPF code
$(ER_BPF_BUILDDIR)/%.bpf.o: bpf/enhancedrecording/%.bpf.c $(wildcard bpf/*.h) | $(ER_BPF_BUILDDIR)
$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(KERNEL_ARCH) $(BPF_INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o $@
$(LLVM_STRIP) -g $@ # strip useless DWARF info
.PHONY: bpf-er-bytecode
bpf-er-bytecode: $(ER_BPF_BUILDDIR)/command.bpf.o $(ER_BPF_BUILDDIR)/disk.bpf.o $(ER_BPF_BUILDDIR)/network.bpf.o $(ER_BPF_BUILDDIR)/counter_test.bpf.o
# Enable target only if /usr/include/linux/bpf.h exists and clang is installed.
# This is a requirement for building BPF bytecode.
.PHONY: bpf-bytecode
bpf-bytecode: bpf-er-bytecode
bpf-bytecode:
ifneq ($(or $(wildcard /usr/include/linux/bpf.h),$(wildcard /usr/include/bpf/bpf_helpers.h)),"")
else
$(error "libbpf-dev is required to build BPF bytecode")
endif # libbpf-dev installed
ifneq ("$(shell command -v clang --version 2>/dev/null)","")
go generate ./lib/bpf/
else
$(error "clang is required to build BPF bytecode")
endif # clang installed
# bpf-up-to-date checks if the generated BPF bytecode is up to date.
.PHONY: bpf-up-to-date
bpf-up-to-date: must-start-clean/host bpf-bytecode
@if ! git diff --quiet; then \
./build.assets/please-run.sh "bpf bytecode" "make -C build.assets bpf-bytecode"; \
exit 1; \
fi
# Generate vmlinux.h based on the installed kernel
.PHONY: update-vmlinux-h
update-vmlinux-h:
bpftool btf dump file /sys/kernel/btf/vmlinux format c >bpf/vmlinux.h
else
.PHONY: bpf-bytecode
bpf-bytecode:
endif
.PHONY: rdpclient
rdpclient: rustup-toolchain-warning
ifeq ("$(with_rdpclient)", "yes")
@@ -550,10 +552,6 @@ clean: clean-ui clean-build
clean-build:
@echo "---> Cleaning up OSS build artifacts."
rm -rf $(BUILDDIR)
# Check if the variable is set to prevent calling remove on the root directory.
ifneq ($(ER_BPF_BUILDDIR),)
rm -f $(ER_BPF_BUILDDIR)/*.o
endif
-cargo clean
-go clean -cache
rm -f *.gz
@@ -951,7 +949,7 @@ test-env-leakage:
# Runs test prepare steps
.PHONY: test-go-prepare
test-go-prepare: ensure-webassets bpf-bytecode $(TEST_LOG_DIR) $(VERSRC)
test-go-prepare: ensure-webassets rdpclient $(TEST_LOG_DIR) $(VERSRC)
# Runs base unit tests
.PHONY: test-go-unit
@@ -1034,7 +1032,7 @@ test-go-chaos:
#
UNIT_ROOT_REGEX := ^TestRoot
.PHONY: test-go-root
test-go-root: ensure-webassets bpf-bytecode rdpclient $(TEST_LOG_DIR)
test-go-root: ensure-webassets rdpclient $(TEST_LOG_DIR)
test-go-root: FLAGS ?= -race -shuffle on
test-go-root: PACKAGES = $(shell go list $(ADDFLAGS) ./... | grep -v -e e2e -e integration -e integrations/operator)
test-go-root: $(VERSRC)
+13 -29
View File
@@ -6,7 +6,7 @@ This document will help you get started.
#### Before you start here are some useful links:
* https://docs.kernel.org/bpf/ - BPF documentation
* https://github.com/aquasecurity/libbpfgo - Go bindings for libbpf
* https://github.com/cilium/ebpf - Go library to interact with BPF
* https://github.com/iovisor/bcc - BPF Compiler Collection (a lot of examples)
* https://github.com/cilium/cilium - BPF-based networking, security, and observability (more examples)
* https://man7.org/linux/man-pages/man7/bpf-helpers.7.html - BPF helpers documentation
@@ -17,14 +17,16 @@ We support BPFs only on ARM64 and x86_64 architectures.
1. VM with Ubuntu 20.04+ (or any other Linux distro with the kernel 5.8+ - Docker doesn't work for many BPF features)
2. Install `clang` (required for compiling BPF code) - `apt install clang` should be enough
3. Install `libbpf` (required for loading BPF code) - get the required version from our Docker file
You can also use our Dockerfile to build the environment:
```bash
make -C build.assets release-centos7
make -C build.assets bpf-bytecode
```
Note that there have been some issues with command tracing reading commands and arguments in a arm64 VM. You may
need to test/develop on an EC2 instance instead.
## BPF code structure
BPF code can be divided into two parts:
@@ -34,37 +36,24 @@ BPF code can be divided into two parts:
BPF programs are located in `bpf` directory in our repository. They are C like programs with some limitations.
The most important limitation is that you can't use any system calls in BPF programs. You can use only BPF helpers.
The BPF programs are compiled using `clang` 10+, GCC is not supported.
The BPF programs are compiled using `clang` 12+, GCC is not supported.
User space code is located in `lib/bpf` directory. It's written in Go and uses the `aquasecurity/libbpfgo` library to load BPF programs into the kernel.
`aquasecurity/libbpfgo` is a Go wrapper around the `libbpf` library. It's a low-level library that allows you to load BPF programs into the kernel.
Note: `aquasecurity/libbpfgo` is not backwards compatible, you need to use the
exact `libbpf` version it requires. For that reason `aquasecurity/libbpfgo`
doesn't use semantic versioning, instead the tags have the format
`v0.4.5-libbpf-1.0.1`. The first part is the `aquasecurity/libbpfgo` version,
the second part is the `libbpf` version. Using the wrong version of `libbpf`
results in a runtime/compilation error.
### libbpf installation
`aquasecurity/libbpfgo` requires `libbpf` to be installed on the system. The most up-to-date installation instructions can be found
in our Docker file: https://github.com/gravitational/teleport/blob/2fd8f75e38eebf5c6826ef594433e5165c3bfbe1/build.assets/Dockerfile-centos7#L108-L131
Remember that the library must be installed in `/opt` directory not in `/usr/local` or `/usr`.
User space code is located in `lib/bpf` directory. It's written in Go and uses the `cilium/ebpf` library to load BPF programs into the kernel. `cilium/ebpf ` is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.
### BPF license
BPF programs are compiled into ELF files. ELF files have a license field that is used to verify that the BPF program
is allowed to run in the kernel. The license field is set to `GPL` by default. Teleport uses `Dual BSD/GPL` which
is allowed to run in the kernel. The license field is set to `GPL` by default. Teleport uses `Dual BSD/GPL` which
disables some BPF features (like logging). To enable all BPF features set the license to `GPL` in the BPF program
and revert back before merging the code.
### Logging
BPF programs can log messages to the kernel log. To enable logging, you need to set the license to `GPL`. Then you
can use `bpf_printk` helper to log messages. The messages will be logged to the kernel log. You can get the messages
from `/sys/kernel/debug/tracing/trace_pipe`. Here is the best explanation that I found so far https://nakryiko.com/posts/bpf-tips-printk/.
BPF programs can log messages to the kernel log. Then you use `bpf_printk` helper to log messages. The messages will be logged to the kernel log. You can get the messages
from `/sys/kernel/debug/tracing/trace_pipe`. Uncomment the line in `common.h` the defines `PRINT_DEBUG_MSGS` or else `bpf_printk` calls
will be compiled out.
Here is the best explanation that I found so far https://nakryiko.com/posts/bpf-tips-printk/.
### Communication between BPF programs and user space
@@ -72,10 +61,6 @@ BPF programs can communicate with user space using maps. Maps are key-value stor
BPF programs and user space. BPF programs can only access maps using BPF helpers. User space can access maps using
`aquasecurity/libbpfgo` library. Maps are defined in BPF programs and can be referenced by name from user space code.
Example:
* BPF: https://github.com/gravitational/teleport/blob/2fd8f75e38eebf5c6826ef594433e5165c3bfbe1/bpf/enhancedrecording/command.bpf.c#L18
* User space: https://github.com/gravitational/teleport/blob/2fd8f75e38eebf5c6826ef594433e5165c3bfbe1/lib/bpf/common_linux.go#L40-L51
## BPF in Teleport
@@ -86,4 +71,3 @@ the kernel 5.8+. Enhanced session recording records all:
* network connections
All events are recorded in the audit log. See https://goteleport.com/docs/enroll-resources/server-access/guides/bpf-session-recording/.
-21
View File
@@ -30,8 +30,6 @@ Vagrant.configure("2") do |config|
#
# * clang: required to build BPF
# * go: 1.21.x (Snap, any version >= 1.21 works)
# * libbpf-dev: 0.5.0 (not installed)
# * libelf, zlib: required by libbpf
# * linux-tools-generic: bpftool
# * llvm: llvm-strip
config.vm.provision "shell", name: "packages", inline: <<-SHELL
@@ -40,7 +38,6 @@ Vagrant.configure("2") do |config|
build-essential \
ca-certificates \
clang \
libelf-dev \
"linux-tools-$(uname -r)" \
linux-tools-generic \
llvm \
@@ -51,24 +48,6 @@ Vagrant.configure("2") do |config|
go version
SHELL
# Clone, build and install libbpf.
#
# Needs to be installed at "/usr/libbpf-$VERSION" for Teleport to be happy.
# Eg, "/usr/libbpf-1.0.1".
config.vm.provision "shell", name: "libbpf", privileged: false, inline: <<-SHELL
LIBBPF_VERSION="$(grep LIBBPF_VERSION /vagrant/build.assets/versions.mk | awk '{print $3}')"
DEST="/usr/libbpf-$LIBBPF_VERSION"
echo "LIBBPF_VERSION = $LIBBPF_VERSION" # eg, 1.0.1
cd ~
[[ ! -d libbpf ]] && git clone --depth 1 https://github.com/libbpf/libbpf.git -b "v$LIBBPF_VERSION"
cd libbpf/src
make
sudo make install DESTDIR="/opt/libbpf"
sudo rm -fr "$DEST"
sudo mv /opt/libbpf/usr "$DEST" && sudo rm -fr /opt/libbpf
SHELL
# Start sessions at /vagrant.
config.vm.provision "shell", name: "user", privileged: false, inline: <<-SHELL
if ! grep -q 'cd /vagrant' ~/.bashrc; then
+15 -6
View File
@@ -31,7 +31,7 @@ enum event_type {
struct common_data_t {
u64 pid;
u64 ppid;
char comm[TASK_COMM_LEN];
u8 command[TASK_COMM_LEN];
u64 cgroup;
};
@@ -40,13 +40,22 @@ struct data_t {
u64 pid;
// ppid is the userspace term (i.e task->real_parent->tgid in kernel).
u64 ppid;
char comm[TASK_COMM_LEN];
// Command is the executable.
u8 command[TASK_COMM_LEN];
// Type is the type of event.
enum event_type type;
char argv[ARGSIZE];
// Argv is the list of arguments to the program.
u8 argv[ARGSIZE];
// ReturnCode is the return code of execve.
int retval;
// CgroupID is the internal cgroupv2 ID of the event.
u64 cgroup;
};
// Force emitting struct data_t into the ELF. bpf2go needs this
// to generate the Go bindings.
const struct data_t *unused __attribute__((unused));
BPF_RING_BUF(execve_events, EVENTS_BUF_SIZE);
BPF_COUNTER(lost);
@@ -67,7 +76,7 @@ static int __submit_arg(void *ptr, struct common_data_t *common)
data->pid = common->pid;
data->cgroup = common->cgroup;
for (int i = 0; i < TASK_COMM_LEN; i++)
data->comm[i] = common->comm[i];
data->command[i] = common->command[i];
bpf_ringbuf_submit(data, 0);
return 1;
@@ -104,7 +113,7 @@ static int enter_execve(const char *filename,
task = (struct task_struct *)bpf_get_current_task();
common.ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&common.comm, sizeof(common.comm));
bpf_get_current_comm(&common.command, sizeof(common.command));
if(__submit_arg((void *)filename, &common) < 0) {
INCR_COUNTER(lost);
@@ -156,7 +165,7 @@ static int exit_execve(int ret)
task = (struct task_struct *)bpf_get_current_task();
data->ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&data->comm, sizeof(data->comm));
bpf_get_current_comm(&data->command, sizeof(data->command));
data->type = EVENT_RET;
data->retval = ret;
+33 -1
View File
@@ -1,7 +1,39 @@
#ifndef BPF_COMMON_H
#define BPF_COMMON_H
// Uncomment to enable debug messages via bpf_printk
// #define PRINT_DEBUG_MSGS
// Maximum monitored sessions.
#define MAX_MONITORED_SESSIONS 1024
#endif // BPF_COMMON_H
#include <bpf/bpf_core_read.h>
// Easier to use bpf_printk taken from https://nakryiko.com/posts/bpf-tips-printk/
// Define our own struct definition if our vmlinux.h is outdated; this empty
// definition will not conflict because of the ___x but anything after the
// triple underscore will get ignored by BPF CO-RE.
struct trace_event_raw_bpf_trace_printk___x {};
#undef bpf_printk
#ifdef PRINT_DEBUG_MSGS
#define bpf_printk(fmt, ...) \
({ \
static char ____fmt[] = fmt "\0"; \
if (bpf_core_type_exists(struct trace_event_raw_bpf_trace_printk___x)) \
{ \
bpf_trace_printk(____fmt, sizeof(____fmt) - 1, ##__VA_ARGS__); \
} \
else \
{ \
____fmt[sizeof(____fmt) - 2] = '\n'; \
bpf_trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
} \
})
#else
#define bpf_printk(fmt, ...)
#endif
#endif // BPF_COMMON_H
-29
View File
@@ -1,29 +0,0 @@
#include "../vmlinux.h"
#include <bpf/bpf_helpers.h> /* most used helpers: SEC, __always_inline, etc */
#include <bpf/bpf_core_read.h> /* for BPF CO-RE helpers */
#include <bpf/bpf_tracing.h> /* for getting kprobe arguments */
#include "../helpers.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
BPF_COUNTER(test_counter);
SEC("tp/syscalls/sys_close")
int tracepoint__syscalls__sys_enter_close(struct syscall_trace_enter *tp)
{
int fd = (int)tp->args[0];
// Special bad FD we trigger upon
if (fd == 1234) {
INCR_COUNTER(test_counter);
}
return 0;
}
SEC("tp/syscalls/sys_exit_close")
int tracepoint__syscalls__sys_exit_close(struct syscall_trace_exit *tp)
{
return 0;
}
+26 -12
View File
@@ -25,14 +25,24 @@ struct val_t {
};
struct data_t {
// CgroupID is the internal cgroupv2 ID of the event.
u64 cgroup;
// PID is the ID of the process.
u64 pid;
int ret;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
// Return_code is the return code of open.
int return_code;
// Command is name of the executable opening the file.
u8 command[TASK_COMM_LEN];
// File_path is the full path to the file being opened.
u8 file_path[NAME_MAX];
// Flags are the flags passed to open.
int flags;
};
// Force emitting struct data_t into the ELF. bpf2go needs this
// to generate the Go bindings.
const struct data_t *unused __attribute__((unused));
BPF_HASH(infotmp, u64, struct val_t, INFLIGHT_MAX);
// hashmap keeps all cgroups id that should be monitored by Teleport.
@@ -46,6 +56,15 @@ BPF_COUNTER(lost);
static int enter_open(const char *filename, int flags) {
struct val_t val = {};
u64 id = bpf_get_current_pid_tgid();
u64 cgroup = bpf_get_current_cgroup_id();
u64 *is_monitored;
// Check if the cgroup should be monitored.
is_monitored = bpf_map_lookup_elem(&monitored_cgroups, &cgroup);
if (is_monitored == NULL) {
// cgroup has not been marked for monitoring, ignore.
return 0;
}
val.pid = id >> 32;
val.fname = filename;
@@ -76,15 +95,15 @@ static int exit_open(int ret) {
return 0;
}
if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) != 0) {
data.comm[0] = '\0';
if (bpf_get_current_comm(&data.command, sizeof(data.command)) != 0) {
data.command[0] = '\0';
}
bpf_probe_read_user(&data.fname, sizeof(data.fname), (void *)valp->fname);
bpf_probe_read_user(&data.file_path, sizeof(data.file_path), (void *)valp->fname);
data.pid = valp->pid;
data.flags = valp->flags;
data.ret = ret;
data.return_code = ret;
data.cgroup = cgroup;
if (bpf_ringbuf_output(&open_events, &data, sizeof(data), 0) != 0)
@@ -110,9 +129,6 @@ int tracepoint__syscalls__sys_exit_creat(struct syscall_trace_exit *tp)
return exit_open(tp->ret);
}
// ARM64 does not implement sys_enter_open only sys_enter_openat. x86 implements it for legacy reasons.
#ifndef __TARGET_ARCH_arm64
SEC("tp/syscalls/sys_enter_open")
int tracepoint__syscalls__sys_enter_open(struct syscall_trace_enter *tp)
{
@@ -122,8 +138,6 @@ int tracepoint__syscalls__sys_enter_open(struct syscall_trace_enter *tp)
return enter_open(filename, flags);
};
#endif // __aarch64__
SEC("tp/syscalls/sys_exit_open")
int tracepoint__syscalls__sys_exit_open(struct syscall_trace_exit *tp)
{
+27 -5
View File
@@ -26,27 +26,49 @@ BPF_HASH(monitored_cgroups, u64, int64_t, MAX_MONITORED_SESSIONS);
// separate data structs for ipv4 and ipv6
struct ipv4_data_t {
// CgroupID is the internal cgroupv2 ID of the event.
u64 cgroup;
// Version is the version of TCP (4 or 6).
u64 ip;
// PID is the process ID.
u32 pid;
// SrcAddr is the source IP address.
u32 saddr;
// DstAddr is the destination IP address.
u32 daddr;
// DstPort is the port the connection is being made to.
u16 dport;
char task[TASK_COMM_LEN];
// Command is name of the executable making the connection.
u8 command[TASK_COMM_LEN];
};
BPF_RING_BUF(ipv4_events, EVENTS_BUF_SIZE);
// Force emitting struct ipv4_data_t into the ELF. bpf2go needs this
// to generate the Go bindings.
const struct ipv4_data_t *unused_ipv4_data_t __attribute__((unused));
struct ipv6_data_t {
// CgroupID is the internal cgroupv2 ID of the event.
u64 cgroup;
// Version is the version of TCP (4 or 6).
u64 ip;
// PID is the process ID.
u32 pid;
// SrcAddr is the source IP address.
struct in6_addr saddr;
// DstAddr is the destination IP address.
struct in6_addr daddr;
// DstPort is the port the connection is being made to.
u16 dport;
char task[TASK_COMM_LEN];
// Command is name of the executable making the connection.
u8 command[TASK_COMM_LEN];
};
BPF_RING_BUF(ipv6_events, EVENTS_BUF_SIZE);
// Force emitting struct ipv6_data_t into the ELF. bpf2go needs this
// to generate the Go bindings.
const struct ipv6_data_t *unused_ipv6_data_t __attribute__((unused));
BPF_COUNTER(lost);
static int trace_connect_entry(struct sock *sk)
@@ -75,7 +97,7 @@ static int trace_connect_return(int ret, short ipver)
struct sock **skpp;
skpp = bpf_map_lookup_elem(&currsock, &id);
if (skpp == 0) {
if (skpp == NULL) {
return 0; // missed entry
}
@@ -96,7 +118,7 @@ static int trace_connect_return(int ret, short ipver)
data4.daddr = BPF_CORE_READ(skp, __sk_common.skc_daddr);
data4.dport = __builtin_bswap16(dport);
data4.cgroup = bpf_get_current_cgroup_id();
bpf_get_current_comm(&data4.task, sizeof(data4.task));
bpf_get_current_comm(&data4.command, sizeof(data4.command));
if (bpf_ringbuf_output(&ipv4_events, &data4, sizeof(data4), 0) != 0)
INCR_COUNTER(lost);
@@ -108,7 +130,7 @@ static int trace_connect_return(int ret, short ipver)
data6.dport = __builtin_bswap16(dport);
data6.cgroup = bpf_get_current_cgroup_id();
bpf_get_current_comm(&data6.task, sizeof(data6.task));
bpf_get_current_comm(&data6.command, sizeof(data6.command));
if (bpf_ringbuf_output(&ipv6_events, &data6, sizeof(data6), 0) != 0)
INCR_COUNTER(lost);
}
-24
View File
@@ -69,26 +69,6 @@ RUN git clone --depth=1 https://github.com/Yubico/libfido2.git -b 1.15.0 && \
make install && \
make clean
## LIBBPF #####################################################################
FROM buildpack-deps:22.04 AS libbpf
# Install required dependencies
RUN apt-get update -y --fix-missing && \
apt-get -q -y upgrade && \
apt-get install -q -y --no-install-recommends \
libelf-dev
ARG LIBBPF_VERSION
# BUILD_STATIC_ONLY - builds only static libraries without shared ones
# EXTRA_CFLAGS - additional CFLAGS to pass to the compiler. fPIC is required so the library code can be moved around in memory
# DESTDIR - where to install the library
# V=1 - verbose build
RUN mkdir -p /opt && cd /opt && \
curl -fsSL https://github.com/libbpf/libbpf/archive/refs/tags/v${LIBBPF_VERSION}.tar.gz | tar xz && \
cd /opt/libbpf-${LIBBPF_VERSION}/src && \
BUILD_STATIC_ONLY=y EXTRA_CFLAGS=-fPIC DESTDIR=/opt/libbpf V=1 make install install_uapi_headers
## BUILDBOX ###################################################################
#
# Image layers are ordered according to how slow that layer takes to build and
@@ -333,10 +313,6 @@ ARG BUF_VERSION # eg, "v1.26.1"
RUN VERSION="$BUF_VERSION"; \
go install "github.com/bufbuild/buf/cmd/buf@$VERSION"
# Copy BPF libraries.
ARG LIBBPF_VERSION
COPY --from=libbpf /opt/libbpf/usr /usr/libbpf-${LIBBPF_VERSION}
# Copy libfido2 libraries.
# Do this near the end to take better advantage of the multi-stage build.
COPY --from=libfido2 /usr/local/include/ /usr/local/include/
+47
View File
@@ -0,0 +1,47 @@
# syntax=docker/dockerfile:1
# Minimal container for generating BPF bytecode
# This Dockerfile is used to generate BPF bytecode used
# by enhanced recording.
FROM docker.io/library/debian:12
ENV LANGUAGE="en_US.UTF-8" \
LANG="en_US.UTF-8" \
LC_ALL="en_US.UTF-8" \
LC_CTYPE="en_US.UTF-8" \
DEBIAN_FRONTEND="noninteractive"
# BUILDARCH is automatically set by DOCKER when building the image with Build Kit (MacOS by default).
# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
ARG BUILDARCH
RUN apt -y update && \
apt install -q -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
curl \
libbpf-dev \
llvm && \
apt -y clean && \
rm -rf /var/lib/apt/lists/*
# Install Go.
ARG GOLANG_VERSION
RUN mkdir -p /opt && \
cd /opt && \
curl -fsSL "https://storage.googleapis.com/golang/$GOLANG_VERSION.linux-$BUILDARCH.tar.gz" | tar xz && \
mkdir -p /go/src/github.com/gravitational/teleport && \
chmod a+w /go && \
chmod a+w /var/lib && \
chmod a-w /
ENV GOPATH="/go" \
GOROOT="/opt/go" \
PATH="$PATH:/opt/go/bin:/go/bin:/go/src/github.com/gravitational/teleport/build"
ARG UID
ARG GID
RUN groupadd bpf --gid=$GID -o && \
useradd bpf --uid=$UID --gid=$GID --create-home --shell=/bin/sh
USER bpf
-30
View File
@@ -153,32 +153,6 @@ RUN git clone --depth=1 https://github.com/Yubico/libfido2.git -b 1.15.0 && \
make install && \
make clean
## LIBBPF #####################################################################
FROM --platform=$BUILDPLATFORM base AS libbpf
# Install additional required dependencies.
RUN yum install -y \
elfutils-libelf-devel && \
yum clean all
# Install custom package with -fPIC.
COPY --from=teleport-buildbox-centos7-assets /opt/custom-packages /opt/custom-packages
RUN if [ "${BUILDARCH}" = "arm64" ]; then export BUILDARCH="aarch64"; fi && \
rpm -ivh /opt/custom-packages/elfutils-libelf-devel-static-*.el*.${BUILDARCH}.rpm && \
rm -rf /opt/custom-packages
# Install libbpf - compile with a newer GCC. The one installed by default is not able to compile it.
# BUILD_STATIC_ONLY - builds only static libraries without shared ones
# EXTRA_CFLAGS - additional CFLAGS to pass to the compiler. fPIC is required so the library code can be moved around in memory
# DESTDIR - where to install the library
# V=1 - verbose build
ARG LIBBPF_VERSION
RUN mkdir -p /opt && cd /opt && \
curl -fsSL https://github.com/libbpf/libbpf/archive/refs/tags/v${LIBBPF_VERSION}.tar.gz | tar xz && \
cd /opt/libbpf-${LIBBPF_VERSION}/src && \
scl enable ${DEVTOOLSET} "BUILD_STATIC_ONLY=y EXTRA_CFLAGS=-fPIC DESTDIR=/opt/libbpf V=1 make install install_uapi_headers"
## LIBPCSCLITE #####################################################################
FROM --platform=$BUILDPLATFORM base AS libpcsclite
@@ -320,10 +294,6 @@ COPY --from=libpcsclite \
# Set LIBRARY_PATH so that libpcsclite can be found by the linker.
ENV LIBRARY_PATH=/usr/local/lib:${LIBRARY_PATH}
# Copy libbpf into the final image.
ARG LIBBPF_VERSION
COPY --from=libbpf /opt/libbpf/usr /usr/libbpf-${LIBBPF_VERSION}
# Copy the pre-built CentOS 7 assets with clang. Needed to build BoringSSL and BPF tools.
COPY --from=teleport-buildbox-centos7-assets /opt/llvm /opt/llvm
# Bring in our custom ninja build, needed for BorinSSL.
+12 -2
View File
@@ -187,7 +187,6 @@ buildbox:
--build-arg WASM_OPT_VERSION=$(WASM_OPT_VERSION) \
--build-arg WASM_BINDGEN_VERSION=$(WASM_BINDGEN_VERSION) \
--build-arg NODE_VERSION=$(NODE_VERSION) \
--build-arg LIBBPF_VERSION=$(LIBBPF_VERSION) \
--build-arg BUF_VERSION=$(BUF_VERSION) \
--build-arg GOGO_PROTO_TAG=$(GOGO_PROTO_TAG) \
--build-arg NODE_GRPC_TOOLS_VERSION=$(NODE_GRPC_TOOLS_VERSION) \
@@ -229,7 +228,6 @@ buildbox-centos7:
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
--build-arg RUST_VERSION=$(RUST_VERSION) \
--build-arg DEVTOOLSET=$(DEVTOOLSET) \
--build-arg LIBBPF_VERSION=$(LIBBPF_VERSION) \
--build-arg LIBPCSCLITE_VERSION=$(LIBPCSCLITE_VERSION) \
--cache-to type=inline \
--cache-from $(BUILDBOX_CENTOS7) \
@@ -356,6 +354,18 @@ fix-imports: buildbox
$(DOCKERFLAGS) -t $(BUILDBOX) \
make -C /go/src/github.com/gravitational/teleport fix-imports/host
# Generate BPF bytecode in a container, so local setup is not required.
.PHONY: bpf-bytecode
bpf-bytecode:
docker build \
--build-arg UID=$(UID) \
--build-arg GID=$(GID) \
--build-arg BUILDARCH=$(RUNTIME_ARCH) \
--build-arg GOLANG_VERSION=$(GOLANG_VERSION) \
-f Dockerfile-bpf -t bpf-builder .
docker run --rm -v "$$(pwd)/../":/teleport -w /teleport bpf-builder \
make bpf-bytecode
#
# Removes the docker image
#
-1
View File
@@ -9,7 +9,6 @@ GOLANGCI_LINT_VERSION ?= v2.4.0
NODE_VERSION ?= 22.21.0
WASM_OPT_VERSION ?= 0.116.1
LIBBPF_VERSION ?= 1.2.2
LIBPCSCLITE_VERSION ?= 1.9.9-teleport
DEVTOOLSET ?= devtoolset-12
+3 -60
View File
@@ -12,100 +12,43 @@
PKGCONF := PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) $(firstword $(shell which pkgconf pkg-config false 2>/dev/null))
# -----------------------------------------------------------------------------
# libbpf detection
#
# Requirements for building with BPF support:
# * clang and llvm-strip programs
# * linux on amd64 or amd64
# * libbpf 1.2.2 (either in /usr/libbpf-1.2.2 or pkg-config)
# * The centos7 buildbox puts this in /usr/libbpf-1.2.2
# * The ng buildbox has a pkg-config file for it
# * Native/local builds have a pkg-config file for it
# * Either using a cross-compiling buildbox or is a native build
#
# The default is without BPF support unless all the critera are met.
#
# TODO(camh): Remove /usr/libbpf-1.2.2 when old buildboxes are replaced by ng
with_bpf := no
BPF_MESSAGE := without-BPF-support
CLANG ?= $(shell which clang || which clang-12)
LLVM_STRIP ?= $(shell which llvm-strip || which llvm-strip-12)
# libbpf version required by the build.
LIBBPF_VER := 1.2.2
FOUND_LIBBPF :=
ifneq (,$(wildcard /usr/libbpf-$(LIBBPF_VER)))
FOUND_LIBBPF := true
LIBBPF_INCLUDES := -I/usr/libbpf-$(LIBBPF_VER)/include
LIBBPF_LIBS := -L/usr/libbpf-$(LIBBPF_VER)/lib64 -lbpf
# libbpf needs libelf. Try to find it with pkg-config/pkgconf and fallback to
# hard-coded defaults if pkg-config says nothing.
LIBBPF_LIBS += $(or $(shell $(PKGCONF) --silence-errors --static --libs libelf),-lelf -lz)
else ifneq (,$(shell $(PKGCONF) --exists 'libbpf = $(LIBBPF_VER)' && echo true))
FOUND_LIBBPF := true
LIBBPF_INCLUDES := $(shell $(PKGCONF) --cflags libbpf)
LIBBPF_LIBS := $(shell $(PKGCONF) --libs --static libbpf)
endif
# Is this build targeting the same OS & architecture it is being compiled on, or
# will it require cross-compilation? We need to know this (especially for ARM) so we
# can set the cross-compiler path (and possibly feature flags) correctly.
IS_NATIVE_BUILD ?= $(filter $(ARCH),$(shell go env GOARCH))
IS_CROSS_COMPILE_BB = $(filter $(BUILDBOX_MODE),cross)
# Only build with BPF if clang and llvm-strip are installed.
ifneq (,$(and $(CLANG),$(LLVM_STRIP)))
# Only build with BPF for linux/amd64 and linux/arm64.
# Other builds have compilation issues that require fixing.
ifneq (,$(filter $(OS)/$(ARCH),linux/amd64 linux/arm64))
# Only build with BPF if we found the right version installed
ifneq (,$(FOUND_LIBBPF))
# Only build with BPF if its a native build or in a cross-compiling buildbox.
ifneq (,$(or $(IS_NATIVE_BUILD),$(IS_CROSS_COMPILE_BB)))
with_bpf := yes
BPF_TAG := bpf
BPF_MESSAGE := with-BPF-support
KERNEL_ARCH := $(shell uname -m | sed 's/x86_64/x86/g; s/aarch64/arm64/g')
ER_BPF_BUILDDIR := lib/bpf/bytecode
BPF_INCLUDES := $(LIBBPF_INCLUDES)
STATIC_LIBS += $(LIBBPF_LIBS)
INCLUDES :=
# Get Clang's default includes on this system. We'll explicitly add these dirs
# to the includes list when compiling with `-target bpf` because otherwise some
# architecture-specific dirs will be "missing" on some architectures/distros -
# headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h,
# sys/cdefs.h etc. might be missing.
#
# Use '-idirafter': Don't interfere with include mechanics except where the
# build would have failed anyways.
CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - </dev/null 2>&1 \
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')
# Link static version of libraries required by Teleport (bpf, pcsc) to reduce
# system dependencies. Avoid dependencies on dynamic libraries if we already
# link the static version using --as-needed.
CGOFLAG = CGO_ENABLED=1 CGO_CFLAGS="$(BPF_INCLUDES)" CGO_LDFLAGS="-Wl,-Bstatic $(STATIC_LIBS) -Wl,-Bdynamic -Wl,--as-needed"
# Link static version of libraries required by Teleport (pcsc) to reduce system dependencies. Avoid dependencies on dynamic libraries if we already link the static version using --as-needed.
CGOFLAG = CGO_ENABLED=1 CGO_LDFLAGS="-Wl,-Bstatic $(STATIC_LIBS) -Wl,-Bdynamic -Wl,--as-needed"
CGOFLAG_TSH = CGO_ENABLED=1 CGO_LDFLAGS="-Wl,-Bstatic $(STATIC_LIBS_TSH) -Wl,-Bdynamic -Wl,--as-needed"
endif # IS_NATIVE_BUILD || IS_CROSS_COMPILE_BB
endif # libbpf found
endif # OS/ARCH == linux/amd64 OR linux/arm64
endif # clang and llvm-strip found
.PHONY: diag-bpf-vars
diag-bpf-vars:
@echo clang: $(CLANG)
@echo llvm-strip: $(LLVM_STRIP)
@echo os/arch: $(OS)-$(ARCH)
@echo found bpf: $(FOUND_LIBBPF)
@echo is-native: $(IS_NATIVE_BUILD)
@echo is-cross: $(IS_CROSS_COMPILE_BB)
@echo buildbox-mode: $(BUILDBOX_MODE)
+1 -1
View File
@@ -53,7 +53,6 @@ require (
github.com/alecthomas/kingpin/v2 v2.4.0 // replaced
github.com/alicebob/miniredis/v2 v2.35.0
github.com/andybalholm/brotli v1.2.0
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2
github.com/armon/go-radix v1.0.0
github.com/aws/aws-sdk-go-v2 v1.39.6
github.com/aws/aws-sdk-go-v2/config v1.31.16
@@ -106,6 +105,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/cilium/ebpf v0.19.0
github.com/coreos/go-semver v0.3.1
github.com/coreos/go-systemd/v22 v22.6.0
github.com/creack/pty v1.1.24
+6 -2
View File
@@ -778,8 +778,6 @@ github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4x
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/apache/thrift v0.22.0 h1:r7mTJdj51TMDe6RtcmNdQxgn9XcyfGDOzegMDRg47uc=
github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJeynQ+/g=
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2 h1:Y7QB6jUsMyr0Bd+rAj67X2/ezNYLuxwp3kkjw5M3Q+4=
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2/go.mod h1:0rEApF1YBHGuZ4C8OYI9q5oDBVpgqtRqYATePl9mCDk=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -1009,6 +1007,8 @@ github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
@@ -1274,6 +1274,8 @@ github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-piv/piv-go/v2 v2.4.0 h1:xamQ/fR4MJiw/Ndbk6yi7MVwhjrwlnDAPuaH9zcGb+I=
github.com/go-piv/piv-go/v2 v2.4.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
@@ -1711,6 +1713,8 @@ github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+1 -1
View File
@@ -80,7 +80,6 @@ require (
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
@@ -152,6 +151,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/charlievieth/strcase v0.0.5 // indirect
github.com/cilium/ebpf v0.19.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/containerd/containerd v1.7.29 // indirect
+6 -2
View File
@@ -785,8 +785,6 @@ github.com/apache/thrift v0.22.0/go.mod h1:1e7J/O1Ae6ZQMTYdy9xa3w9k+XHWPfRvdPyJe
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2 h1:Y7QB6jUsMyr0Bd+rAj67X2/ezNYLuxwp3kkjw5M3Q+4=
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2/go.mod h1:0rEApF1YBHGuZ4C8OYI9q5oDBVpgqtRqYATePl9mCDk=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -975,6 +973,8 @@ github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
@@ -1231,6 +1231,8 @@ github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-piv/piv-go/v2 v2.4.0 h1:xamQ/fR4MJiw/Ndbk6yi7MVwhjrwlnDAPuaH9zcGb+I=
github.com/go-piv/piv-go/v2 v2.4.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -1696,6 +1698,8 @@ github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+1 -1
View File
@@ -84,7 +84,6 @@ require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apparentlymart/go-textseg v1.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
@@ -156,6 +155,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/charlievieth/strcase v0.0.5 // indirect
github.com/cilium/ebpf v0.19.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/containerd/containerd v1.7.29 // indirect
+6 -2
View File
@@ -802,8 +802,6 @@ github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJE
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2 h1:Y7QB6jUsMyr0Bd+rAj67X2/ezNYLuxwp3kkjw5M3Q+4=
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2/go.mod h1:0rEApF1YBHGuZ4C8OYI9q5oDBVpgqtRqYATePl9mCDk=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -998,6 +996,8 @@ github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
@@ -1263,6 +1263,8 @@ github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-piv/piv-go/v2 v2.4.0 h1:xamQ/fR4MJiw/Ndbk6yi7MVwhjrwlnDAPuaH9zcGb+I=
github.com/go-piv/piv-go/v2 v2.4.0/go.mod h1:ShZi74nnrWNQEdWzRUd/3cSig3uNOcEZp+EWl0oewnI=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
@@ -1750,6 +1752,8 @@ github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+75 -103
View File
@@ -20,21 +20,19 @@
package bpf
import "C"
import (
"bytes"
"context"
"embed"
"encoding/binary"
"errors"
"net"
"slices"
"strconv"
"sync"
"time"
"unsafe"
"github.com/cilium/ebpf/ringbuf"
"github.com/gravitational/trace"
"golang.org/x/sys/unix"
ossteleport "github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/constants"
@@ -45,45 +43,17 @@ import (
"github.com/gravitational/teleport/lib/utils"
)
//go:embed bytecode
var embedFS embed.FS
// ArgsCacheSize is the number of args events to store before dropping args
// events.
const ArgsCacheSize = 1024
// SessionWatch is a map of cgroup IDs that the BPF service is watching and
// emitting events for.
type SessionWatch struct {
watch map[uint64]*SessionContext
mu sync.Mutex
type sessionEnder interface {
endSession(cgroupID uint64) error
}
func NewSessionWatch() SessionWatch {
return SessionWatch{
watch: make(map[uint64]*SessionContext),
}
}
func (w *SessionWatch) Get(cgroupID uint64) (ctx *SessionContext, ok bool) {
w.mu.Lock()
defer w.mu.Unlock()
ctx, ok = w.watch[cgroupID]
return
}
func (w *SessionWatch) Add(cgroupID uint64, ctx *SessionContext) {
w.mu.Lock()
defer w.mu.Unlock()
w.watch[cgroupID] = ctx
}
func (w *SessionWatch) Remove(cgroupID uint64) {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.watch, cgroupID)
type cgroupRegister interface {
startSession(cgroupID uint64) error
endSession(cgroupID uint64) error
}
// Service manages BPF and control groups orchestration.
@@ -92,7 +62,7 @@ type Service struct {
// watch is a map of cgroup IDs that the BPF service is watching and
// emitting events for.
watch SessionWatch
watch utils.SyncMap[uint64, *SessionContext]
// argsCache holds the arguments to execve because they come a different
// event than the result.
@@ -123,18 +93,17 @@ func New(config *servicecfg.BPFConfig) (bpf BPF, err error) {
return nil, trace.Wrap(err)
}
closeContext, closeFunc := context.WithCancel(context.Background())
// If BPF-based auditing is not enabled, don't configure anything return
// right away.
if !config.Enabled {
logger.DebugContext(closeContext, "Enhanced session recording is not enabled, skipping")
logger.DebugContext(context.Background(), "Enhanced session recording is not enabled, skipping")
return &NOP{}, nil
}
closeContext, closeFunc := context.WithCancel(context.Background())
s := &Service{
BPFConfig: config,
watch: NewSessionWatch(),
closeContext: closeContext,
closeFunc: closeFunc,
}
@@ -165,20 +134,18 @@ func New(config *servicecfg.BPFConfig) (bpf BPF, err error) {
start := time.Now()
logger.DebugContext(closeContext, "Starting enhanced session recording")
// Compile and start BPF programs if they are enabled (buffer size given).
// Compile and start BPF programs (buffer size given).
s.exec, err = startExec(*config.CommandBufferSize)
if err != nil {
return nil, trace.Wrap(err)
return nil, trace.Wrap(err, "failed to load command hooks")
}
s.open, err = startOpen(*config.DiskBufferSize)
if err != nil {
return nil, trace.Wrap(err)
return nil, trace.Wrap(err, "failed to load disk hooks")
}
// Load network BPF modules only when required.
s.conn, err = startConn(*config.NetworkBufferSize)
if err != nil {
return nil, trace.Wrap(err)
return nil, trace.Wrap(err, "failed to load network hooks")
}
logger.DebugContext(closeContext, "Started enhanced session recording",
@@ -208,9 +175,7 @@ func (s *Service) Close(restarting bool) error {
// Unload the BPF programs.
s.exec.close()
s.open.close()
if s.conn != nil {
s.conn.close()
}
s.conn.close()
// Close cgroup service. We should not unmount the cgroup filesystem if
// we're restarting.
@@ -239,7 +204,7 @@ func (s *Service) OpenSession(ctx *SessionContext) (uint64, error) {
}
// initializedModClosures holds all already opened modules closures.
initializedModClosures := make([]interface{ endSession(uint64) error }, 0)
initializedModClosures := make([]sessionEnder, 0)
for _, module := range []cgroupRegister{
s.open,
s.exec,
@@ -259,7 +224,7 @@ func (s *Service) OpenSession(ctx *SessionContext) (uint64, error) {
}
// Start watching for any events that come from this cgroup.
s.watch.Add(cgroupID, ctx)
s.watch.Store(cgroupID, ctx)
// Place requested PID into cgroup.
err = s.cgroup.Place(ctx.SessionID, ctx.PID)
@@ -279,7 +244,7 @@ func (s *Service) CloseSession(ctx *SessionContext) error {
}
// Stop watching for events from this PID.
s.watch.Remove(cgroupID)
s.watch.Delete(cgroupID)
var errs []error
// Move all PIDs to the root cgroup and remove the cgroup created for this
@@ -288,7 +253,7 @@ func (s *Service) CloseSession(ctx *SessionContext) error {
errs = append(errs, trace.Wrap(err))
}
for _, module := range []interface{ endSession(cgroupID uint64) error }{
for _, module := range []sessionEnder{
s.open,
s.exec,
s.conn,
@@ -306,6 +271,24 @@ func (s *Service) Enabled() bool {
return true
}
func sendEvents(bpfEvents chan []byte, eventBuf *ringbuf.Reader) {
defer eventBuf.Close()
for {
rec, err := eventBuf.Read()
if err != nil {
if errors.Is(err, ringbuf.ErrClosed) {
logger.DebugContext(context.Background(), "Received signal, exiting")
return
}
logger.ErrorContext(context.Background(), "Error reading from ring buffer", "error", err)
return
}
bpfEvents <- rec.RawSample[:]
}
}
// processAccessEvents pulls events off the perf ring buffer, parses them, and emits them to
// the audit log.
func (s *Service) processAccessEvents() {
@@ -343,7 +326,7 @@ func (s *Service) processNetworkEvents() {
// emitCommandEvent will parse and emit command events to the Audit Log.
func (s *Service) emitCommandEvent(eventBytes []byte) {
// Unmarshal raw event bytes.
var event rawExecEvent
var event commandDataT
err := unmarshalEvent(eventBytes, &event)
if err != nil {
logger.DebugContext(s.closeContext, "Failed to read binary data", "error", err)
@@ -351,7 +334,7 @@ func (s *Service) emitCommandEvent(eventBytes []byte) {
}
// If the event comes from a unmonitored process/cgroup, don't process it.
ctx, ok := s.watch.Get(event.CgroupID)
ctx, ok := s.watch.Load(event.Cgroup)
if !ok {
return
}
@@ -366,7 +349,7 @@ func (s *Service) emitCommandEvent(eventBytes []byte) {
// Args are sent in their own event by execsnoop to save stack space. Store
// the args in a ttlmap, so they can be retrieved when the return event arrives.
case eventArg:
key := strconv.FormatUint(event.PID, 10)
key := strconv.FormatUint(event.Pid, 10)
args, err := utils.FnCacheGet(s.closeContext, s.argsCache, key, func(ctx context.Context) ([]string, error) {
return make([]string, 0), nil
@@ -376,19 +359,17 @@ func (s *Service) emitCommandEvent(eventBytes []byte) {
args = []string{}
}
argv := (*C.char)(unsafe.Pointer(&event.Argv))
args = append(args, C.GoString(argv))
args = append(args, ConvertString(event.Argv[:]))
s.argsCache.SetWithTTL(key, args, 24*time.Hour)
// The event has returned, emit the fully parsed event.
case eventRet:
// The args should have come in a previous event, find them by PID.
key := strconv.FormatUint(event.PID, 10)
key := strconv.FormatUint(event.Pid, 10)
args, err := utils.FnCacheGet(s.closeContext, s.argsCache, key, func(ctx context.Context) ([]string, error) {
return nil, trace.NotFound("args missing")
})
if err != nil {
logger.DebugContext(s.closeContext, "Got event with missing args, skipping")
lostCommandEvents.Add(float64(1))
@@ -418,12 +399,12 @@ func (s *Service) emitCommandEvent(eventBytes []byte) {
UserTraits: ctx.UserTraits.Clone(),
},
BPFMetadata: apievents.BPFMetadata{
CgroupID: event.CgroupID,
Program: ConvertString(unsafe.Pointer(&event.Command)),
PID: event.PID,
CgroupID: event.Cgroup,
Program: ConvertString(event.Command[:]),
PID: event.Pid,
},
PPID: event.PPID,
ReturnCode: event.ReturnCode,
PPID: event.Ppid,
ReturnCode: event.Retval,
Path: args[0],
Argv: args[1:],
}
@@ -439,7 +420,7 @@ func (s *Service) emitCommandEvent(eventBytes []byte) {
// emitDiskEvent will parse and emit disk events to the Audit Log.
func (s *Service) emitDiskEvent(eventBytes []byte) {
// Unmarshal raw event bytes.
var event rawOpenEvent
var event diskDataT
err := unmarshalEvent(eventBytes, &event)
if err != nil {
logger.DebugContext(s.closeContext, "Failed to read binary data", "error", err)
@@ -447,12 +428,12 @@ func (s *Service) emitDiskEvent(eventBytes []byte) {
}
// If the event comes from a unmonitored process/cgroup, don't process it.
ctx, ok := s.watch.Get(event.CgroupID)
ctx, ok := s.watch.Load(event.Cgroup)
if !ok {
return
}
// If the network event is not being monitored, don't process it.
// If the disk event is not being monitored, don't process it.
_, ok = ctx.Events[constants.EnhancedRecordingDisk]
if !ok {
return
@@ -480,12 +461,12 @@ func (s *Service) emitDiskEvent(eventBytes []byte) {
UserTraits: ctx.UserTraits.Clone(),
},
BPFMetadata: apievents.BPFMetadata{
CgroupID: event.CgroupID,
Program: ConvertString(unsafe.Pointer(&event.Command)),
PID: event.PID,
CgroupID: event.Cgroup,
Program: ConvertString(event.Command[:]),
PID: event.Pid,
},
Flags: event.Flags,
Path: ConvertString(unsafe.Pointer(&event.Path)),
Path: ConvertString(event.FilePath[:]),
ReturnCode: event.ReturnCode,
}
// Logs can be DoS by event failures here
@@ -495,7 +476,7 @@ func (s *Service) emitDiskEvent(eventBytes []byte) {
// emit4NetworkEvent will parse and emit IPv4 events to the Audit Log.
func (s *Service) emit4NetworkEvent(eventBytes []byte) {
// Unmarshal raw event bytes.
var event rawConn4Event
var event networkIpv4DataT
err := unmarshalEvent(eventBytes, &event)
if err != nil {
logger.DebugContext(s.closeContext, "Failed to read binary data", "error", err)
@@ -503,7 +484,7 @@ func (s *Service) emit4NetworkEvent(eventBytes []byte) {
}
// If the event comes from an unmonitored process/cgroup, don't process it.
ctx, ok := s.watch.Get(event.CgroupID)
ctx, ok := s.watch.Load(event.Cgroup)
if !ok {
return
}
@@ -514,8 +495,8 @@ func (s *Service) emit4NetworkEvent(eventBytes []byte) {
return
}
srcAddr := ipv4HostToIP(event.SrcAddr)
dstAddr := ipv4HostToIP(event.DstAddr)
srcAddr := ipv4HostToIP(event.Saddr)
dstAddr := ipv4HostToIP(event.Daddr)
sessionNetworkEvent := &apievents.SessionNetwork{
Metadata: apievents.Metadata{
Type: events.SessionNetworkEvent,
@@ -538,11 +519,11 @@ func (s *Service) emit4NetworkEvent(eventBytes []byte) {
UserTraits: ctx.UserTraits.Clone(),
},
BPFMetadata: apievents.BPFMetadata{
CgroupID: event.CgroupID,
Program: ConvertString(unsafe.Pointer(&event.Command)),
PID: uint64(event.PID),
CgroupID: event.Cgroup,
Program: ConvertString(event.Command[:]),
PID: uint64(event.Pid),
},
DstPort: int32(event.DstPort),
DstPort: int32(event.Dport),
DstAddr: dstAddr.String(),
SrcAddr: srcAddr.String(),
TCPVersion: 4,
@@ -555,7 +536,7 @@ func (s *Service) emit4NetworkEvent(eventBytes []byte) {
// emit6NetworkEvent will parse and emit IPv6 events to the Audit Log.
func (s *Service) emit6NetworkEvent(eventBytes []byte) {
// Unmarshal raw event bytes.
var event rawConn6Event
var event networkIpv6DataT
err := unmarshalEvent(eventBytes, &event)
if err != nil {
logger.DebugContext(s.closeContext, "Failed to read binary data", "error", err)
@@ -563,7 +544,7 @@ func (s *Service) emit6NetworkEvent(eventBytes []byte) {
}
// If the event comes from an unmonitored process/cgroup, don't process it.
ctx, ok := s.watch.Get(event.CgroupID)
ctx, ok := s.watch.Load(event.Cgroup)
if !ok {
return
}
@@ -574,8 +555,8 @@ func (s *Service) emit6NetworkEvent(eventBytes []byte) {
return
}
srcAddr := ipv6HostToIP(event.SrcAddr)
dstAddr := ipv6HostToIP(event.DstAddr)
srcAddr := net.IP(event.Saddr.In6U.U6Addr8[:])
dstAddr := net.IP(event.Daddr.In6U.U6Addr8[:])
sessionNetworkEvent := &apievents.SessionNetwork{
Metadata: apievents.Metadata{
Type: events.SessionNetworkEvent,
@@ -598,11 +579,11 @@ func (s *Service) emit6NetworkEvent(eventBytes []byte) {
UserTraits: ctx.UserTraits.Clone(),
},
BPFMetadata: apievents.BPFMetadata{
CgroupID: event.CgroupID,
Program: ConvertString(unsafe.Pointer(&event.Command)),
PID: uint64(event.PID),
CgroupID: event.Cgroup,
Program: ConvertString(event.Command[:]),
PID: uint64(event.Pid),
},
DstPort: int32(event.DstPort),
DstPort: int32(event.Dport),
DstAddr: dstAddr.String(),
SrcAddr: srcAddr.String(),
TCPVersion: 6,
@@ -618,15 +599,6 @@ func ipv4HostToIP(addr uint32) net.IP {
return val
}
func ipv6HostToIP(addr [4]uint32) net.IP {
val := make([]byte, 16)
binary.LittleEndian.PutUint32(val[0:], addr[0])
binary.LittleEndian.PutUint32(val[4:], addr[1])
binary.LittleEndian.PutUint32(val[8:], addr[2])
binary.LittleEndian.PutUint32(val[12:], addr[3])
return val
}
// unmarshalEvent will unmarshal the perf event.
func unmarshalEvent(data []byte, v interface{}) error {
err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, v)
@@ -636,9 +608,9 @@ func unmarshalEvent(data []byte, v interface{}) error {
return nil
}
// ConvertString converts a C string to a Go string.
func ConvertString(s unsafe.Pointer) string {
return C.GoString((*C.char)(s))
// ConvertString converts a NUL-terminated string to a Go string.
func ConvertString(s []byte) string {
return unix.ByteSliceToString(s)
}
// SystemHasBPF returns true if the binary was build with support for BPF
+223 -330
View File
@@ -23,25 +23,19 @@ package bpf
import (
"context"
_ "embed"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
osexec "os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
"unsafe"
"github.com/aquasecurity/libbpfgo"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/constants"
@@ -56,8 +50,10 @@ import (
const (
// reexecInCGroupCmd is a cmd used to re-exec the test binary and call arbitrary program.
reexecInCGroupCmd = "reexecCgroup"
// networkInCgroupCmd is a cmd used to re-exec the test binary and make HTTP call.
networkInCgroupCmd = "networkCgroup"
// networkIPv4InCgroupCmd is a cmd used to re-exec the test binary and make HTTP call using an IPv4 address.
networkIPv4InCgroupCmd = "networkCgroupIPv4"
// networkIPv6InCgroupCmd is a cmd used to re-exec the test binary and make HTTP call using an IPv6 address.
networkIPv6InCgroupCmd = "networkCgroupIPv6"
)
func TestMain(m *testing.M) {
@@ -73,16 +69,18 @@ func TestMain(m *testing.M) {
cmd := os.Args[2]
err = waitAndRun(cmd)
case networkInCgroupCmd:
// Get the endpoint to call.
case networkIPv4InCgroupCmd:
endpoint := os.Args[2]
err = callEndpoint(endpoint)
err = getEndpoint(endpoint, false)
case networkIPv6InCgroupCmd:
endpoint := os.Args[2]
err = getEndpoint(endpoint, true)
default:
os.Exit(2)
}
if err != nil {
fmt.Printf("rexec failed: %v\n", err)
// Something went wrong, exit with error.
os.Exit(1)
}
@@ -101,23 +99,24 @@ func waitAndRun(cmd string) error {
return err
}
return osexec.Command(cmd).Run()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return osexec.CommandContext(ctx, cmd).Run()
}
// callEndpoint wait for continue signal to be generated an executes HTTP GET
// on provided endpoint.
func callEndpoint(endpoint string) error {
// getEndpoint wait for continue signal to be generated then creates an
// HTTP GET request on provided endpoint.
func getEndpoint(endpoint string, ipv6 bool) error {
if err := waitForContinue(); err != nil {
return err
}
resp, err := http.Get(endpoint)
if resp != nil {
// Close the body to make our linter happy.
_ = resp.Body.Close()
forceIPVersion := "-4"
if ipv6 {
forceIPVersion = "-6"
}
return err
return osexec.Command("curl", forceIPVersion, endpoint).Run()
}
// waitForContinue opens FD 3 and waits the signal from parent process that
@@ -135,17 +134,9 @@ func waitForContinue() error {
}
func TestRootWatch(t *testing.T) {
// TODO(jakule): Find a way to run this test in CI. Disable for now to not block all BPF tests.
t.Skip("this test always fails when running inside a CGroup/Docker")
// This test must be run as root and the host has to be capable of running
// BPF programs.
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled")
}
if !isRoot() {
t.Skip("Tests for package bpf can only be run as root.")
}
checkBPF(t)
// Create temporary directory where cgroup2 hierarchy will be mounted.
cgroupPath := t.TempDir()
@@ -221,169 +212,128 @@ func TestRootWatch(t *testing.T) {
}
}
// TestRootObfuscate checks if execsnoop can capture Obfuscated commands.
func TestRootObfuscate(t *testing.T) {
t.Skip("flaky test, disable now")
// TestRootScripts checks if execsnoop can capture commands executed
// in scripts, whether they are obfuscated or not.
func TestRootScripts(t *testing.T) {
// This test must be run as root and the host has to be capable of running
// BPF programs.
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled")
}
if !isRoot() {
t.Skip("Tests for package bpf can only be run as root.")
checkBPF(t)
// Create temporary directory where cgroup2 hierarchy will be mounted.
cgroupPath := t.TempDir()
// Create BPF service.
service, err := New(&servicecfg.BPFConfig{
Enabled: true,
CgroupPath: cgroupPath,
})
require.NoError(t, err)
t.Cleanup(func() {
const restarting = false
require.NoError(t, service.Close(restarting))
})
tests := []struct {
name string
scriptContents string
expectedCommand string
usedCommands []string
}{
{
name: "normal script",
scriptContents: "ls -la",
expectedCommand: "ls -la",
usedCommands: []string{"ls"},
},
{
name: "base64 encoded",
scriptContents: fmt.Sprintf("echo %s | base64 --decode | /bin/sh", base64.StdEncoding.EncodeToString([]byte("ls -la"))),
expectedCommand: "ls -la",
usedCommands: []string{"ls"},
},
}
// Find the programs needed to run these tests on the host.
decoderPath, err := osexec.LookPath("base64")
require.NoError(t, err)
shellPath, err := osexec.LookPath("sh")
require.NoError(t, err)
// Start execsnoop.
execsnoop, err := startExec(8192)
defer execsnoop.close()
require.NoError(t, err)
// Create obfuscated script.
shellContents := fmt.Sprintf("#!%v\necho bHM= | %v --decode | %v",
shellPath, decoderPath, shellPath)
// Write script to a temporary folder.
fileName := filepath.Join(t.TempDir(), "test-script")
err = os.WriteFile(fileName, []byte(shellContents), 0700)
require.NoError(t, err)
done := make(chan struct{})
defer close(done)
// Start a goroutine that writes a script which will execute "ls"
// in a loop. Then waits for an exec event to show up the reports "ls"
// has been executed.
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
runCmd(t, reexecInCGroupCmd, fileName, execsnoop, require.NoError)
case <-done:
return
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
usedCommands := append(tt.usedCommands, "bash")
for _, cmd := range usedCommands {
// Find the programs needed to run these tests on the host.
_, err := osexec.LookPath(cmd)
require.NoError(t, err)
}
}
}()
// Wait for an event to arrive from execsnoop. If an event does not arrive
// within 10 seconds, timeout.
for {
select {
case eventBytes := <-execsnoop.events():
var event rawExecEvent
err := unmarshalEvent(eventBytes, &event)
// Create a fake audit log that can be used to capture the events emitted.
emitter := eventstest.NewChannelEmitter(8)
scx := &SessionContext{
Context: t.Context(),
Namespace: apidefaults.Namespace,
SessionID: uuid.New().String(),
ServerID: uuid.New().String(),
ServerHostname: "hostname",
Login: "foo",
User: "foo@example.com",
PID: os.Getpid(),
Emitter: emitter,
Events: map[string]bool{
constants.EnhancedRecordingCommand: true,
},
}
_, err := service.OpenSession(scx)
require.NoError(t, err)
// Check the event is what we expect, in this case "ls".
if ConvertString(unsafe.Pointer(&event.Command)) == "ls" {
return
}
case <-time.After(10 * time.Second):
t.Fatalf("Timed out waiting for an event.")
}
}
}
t.Cleanup(func() { service.CloseSession(scx) })
// TestRootScript checks if execsnoop can capture what a script executes.
func TestRootScript(t *testing.T) {
t.Skip("flaky test, disable now")
// This test must be run as root and the host has to be capable of running
// BPF programs.
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled")
}
if !isRoot() {
t.Skip("Tests for package bpf can only be run as root.")
}
// Write script to a temporary folder.
fileName := filepath.Join(t.TempDir(), "test-script")
err := os.WriteFile(fileName, []byte("#!/bin/sh\nls"), 0700)
require.NoError(t, err)
// Start execsnoop.
execsnoop, err := startExec(8192)
defer execsnoop.close()
require.NoError(t, err)
done := make(chan struct{})
defer close(done)
// Start a goroutine that writes a script which will execute "ls"
// in a loop. Then waits for an exec event to show up the reports "ls"
// has been executed.
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
// Run script in a cgroup.
runCmd(t, reexecInCGroupCmd, fileName, execsnoop, require.NoError)
}
}
}()
// Wait for an event to arrive from execsnoop. If an event does not arrive
// within 10 seconds, timeout.
for {
select {
case eventBytes := <-execsnoop.events():
var event rawExecEvent
err := unmarshalEvent(eventBytes, &event)
// Write script to a temporary folder.
fileName := filepath.Join(t.TempDir(), "test-script")
scriptContents := "#!/bin/bash\n" + tt.scriptContents
err = os.WriteFile(fileName, []byte(scriptContents), 0o700)
require.NoError(t, err)
// Check the event is what we expect, in this case "ls".
if ConvertString(unsafe.Pointer(&event.Command)) == "ls" {
return
err = osexec.CommandContext(t.Context(), fileName).Run()
require.NoError(t, err)
// Wait for an event to arrive from execsnoop. If an event does not arrive
// within 10 seconds, timeout.
for {
select {
case event := <-emitter.C():
cmdEvent, ok := event.(*apievents.SessionCommand)
require.True(t, ok, "expected SessionCommand event, got %T", event)
args := append([]string{cmdEvent.BPFMetadata.Program}, cmdEvent.Argv...)
cmd := strings.Join(args, " ")
t.Logf("got event: %q", cmd)
if cmd == tt.expectedCommand {
return
}
case <-time.After(10 * time.Second):
t.Fatalf("Timed out waiting for an event.")
}
}
case <-time.After(10 * time.Second):
t.Fatalf("Timed out waiting for an event.")
return
}
})
}
}
// TestRootPrograms tests execsnoop, opensnoop, and tcpconnect to make sure they
// run and receive events.
func TestRootPrograms(t *testing.T) {
t.Skip("flaky test, disable now")
// This test must be run as root. Only root can create cgroups.
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled")
}
if !isRoot() {
t.Skip("Tests for package bpf can only be run as root.")
}
// Start a debug server that tcpconnect will connect to.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, world")
}))
defer ts.Close()
checkBPF(t)
// Start execsnoop.
execsnoop, err := startExec(8192)
execsnoop, err := startExec(8)
require.NoError(t, err)
defer execsnoop.close()
// Start opensnoop.
opensnoop, err := startOpen(8192)
opensnoop, err := startOpen(8)
require.NoError(t, err)
defer opensnoop.close()
// Start tcpconnect.
tcpconnect, err := startConn(8192)
tcpconnect, err := startConn(8)
require.NoError(t, err)
defer tcpconnect.close()
@@ -392,20 +342,20 @@ func TestRootPrograms(t *testing.T) {
tests := []struct {
inName string
inEventCh <-chan []byte
genEvents func(t *testing.T, ctx context.Context)
genEvents func(t *testing.T)
verifyFn func(event []byte) bool
}{
// Run execsnoop with "ls".
{
inName: "execsnoop",
inEventCh: execsnoop.events(),
genEvents: func(t *testing.T, ctx context.Context) {
executeCommand(t, ctx, "ls", execsnoop)
genEvents: func(t *testing.T) {
executeCommand(t, "ls", execsnoop)
},
verifyFn: func(event []byte) bool {
var e rawExecEvent
var e commandDataT
err := unmarshalEvent(event, &e)
return err == nil && ConvertString(unsafe.Pointer(&e.Command)) == "ls"
return err == nil && ConvertString(e.Command[:]) == "ls"
},
},
// Run opensnoop with "ls". This is fine because "ls" will open some
@@ -413,127 +363,75 @@ func TestRootPrograms(t *testing.T) {
{
inName: "opensnoop",
inEventCh: opensnoop.events(),
genEvents: func(t *testing.T, ctx context.Context) {
executeCommand(t, ctx, "ls", opensnoop)
genEvents: func(t *testing.T) {
executeCommand(t, "ls", opensnoop)
},
verifyFn: func(event []byte) bool {
var e rawOpenEvent
var e diskDataT
err := unmarshalEvent(event, &e)
return err == nil
return err == nil && ConvertString(e.Command[:]) == "ls"
},
},
// Run tcpconnect with netcat.
// Run tcpconnect with curl forcing IPv4.
{
inName: "tcpconnect",
inName: "tcpconnect ipv4",
inEventCh: tcpconnect.v4Events(),
genEvents: func(t *testing.T, ctx context.Context) {
executeHTTP(t, ctx, ts.URL, tcpconnect)
genEvents: func(t *testing.T) {
executeHTTP(t, "http://google.com", false, tcpconnect)
},
verifyFn: func(event []byte) bool {
var e rawConn4Event
var e networkIpv4DataT
err := unmarshalEvent(event, &e)
return err == nil
return err == nil && ConvertString(e.Command[:]) == "curl"
},
},
// Run tcpconnect with curl forcing IPv6.
{
inName: "tcpconnect ipv6",
inEventCh: tcpconnect.v6Events(),
genEvents: func(t *testing.T) {
executeHTTP(t, "http://google.com", true, tcpconnect)
},
verifyFn: func(event []byte) bool {
var e networkIpv6DataT
err := unmarshalEvent(event, &e)
return err == nil && ConvertString(e.Command[:]) == "curl"
},
},
}
for _, tt := range tests {
// Create a context that will be used to signal that an event has been recieved.
doneContext, doneFunc := context.WithCancel(context.Background())
t.Run(tt.inName, func(t *testing.T) {
// Create a context that will be used to signal that an event has been received.
doneContext, doneFunc := context.WithCancel(context.Background())
t.Cleanup(doneFunc)
// Start two goroutines. The first will wait for the BPF program event to
// arrive, and once it has, signal over the context that it's complete. The
// second will continue to execute or an HTTP GET in a processAccessEvents attempting to
// trigger an event.
go waitForEvent(doneContext, doneFunc, tt.inEventCh, tt.verifyFn)
// Start two goroutines. The first will wait for the BPF program event to
// arrive, and once it has, signal over the context that it's complete. The
// second will continue to execute or an HTTP GET in a processAccessEvents attempting to
// trigger an event.
go waitForEvent(doneContext, doneFunc, tt.inEventCh, tt.verifyFn)
go tt.genEvents(t, doneContext)
go tt.genEvents(t)
// Wait for an event to arrive from execsnoop. If an event does not arrive
// within 10 seconds, timeout.
select {
case <-doneContext.Done():
case <-time.After(10 * time.Second):
t.Fatalf("Timed out waiting for an %v event.", tt.inName)
}
// Wait for an event to arrive from execsnoop. If an event does not arrive
// within 10 seconds, timeout.
select {
case <-doneContext.Done():
case <-time.After(10 * time.Second):
t.Fatalf("Timed out waiting for an %v event.", tt.inName)
}
})
}
}
// TestRootBPFCounter tests that BPF-to-Prometheus counter works ok
func TestRootBPFCounter(t *testing.T) {
t.Skip("flaky test, disable now")
// This test must be run as root. Only root can create cgroups.
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled")
}
if !isRoot() {
t.Skip("Tests for package bpf can only be run as root.")
}
counterTestBPF, err := embedFS.ReadFile("bytecode/counter_test.bpf.o")
if err != nil {
t.Skip(fmt.Sprintf("Tests for package bpf can not be run: %v.", err))
}
module, err := libbpfgo.NewModuleFromBuffer(counterTestBPF, "counter_test")
require.NoError(t, err)
// Load into the kernel
err = module.BPFLoadObject()
require.NoError(t, err)
err = AttachSyscallTracepoint(module, "close")
require.NoError(t, err)
promCounter := prometheus.NewCounter(prometheus.CounterOpts{Name: "test"})
counter, err := NewCounter(module, "test_counter", promCounter)
require.NoError(t, err)
// Make sure the counter starts with 0
require.Zero(t, testutil.ToFloat64(promCounter))
// close(1234) will cause the counter to get incremented.
magicFD := 1234
// First do it a few times as to no overflow the doorbell buffer
gentleBumps := 10
for i := 0; i < gentleBumps; i++ {
syscall.Close(magicFD)
}
// Not ideal but no other good way to know that the counter was updated
time.Sleep(time.Second)
// Make sure all are accounted for
require.Equal(t, float64(gentleBumps), testutil.ToFloat64(promCounter))
// Next, pound the counter to hopefully overflow the doorbell.
poundingBumps := 100000
for i := 0; i < poundingBumps; i++ {
syscall.Close(magicFD)
}
// Not ideal but no other good way to know that the counter was updated
time.Sleep(time.Second)
// Make sure all are accounted for
require.Equal(t, float64(gentleBumps+poundingBumps), testutil.ToFloat64(promCounter))
counter.Close()
}
// TestRootLargeCommands given commands with higher amount of characters
// (length), ensure the command events are generated correctly.
func TestRootLargeCommands(t *testing.T) {
t.Skip("flaky test, disable now")
// This test must be run as root and the host has to be capable of running
// BPF programs.
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled")
}
if !isRoot() {
t.Skip("Tests for package bpf can only be run as root.")
}
checkBPF(t)
for name, test := range map[string]struct {
cmd string
@@ -555,19 +453,20 @@ func TestRootLargeCommands(t *testing.T) {
// Since we're using a random command, we should expect its
// execution will fail.
runCmd(t, reexecInCGroupCmd, test.cmd, execsnoop, require.Error)
err = runCmd(t, reexecInCGroupCmd, test.cmd, execsnoop)
require.Error(t, err)
for {
select {
case eventBytes := <-execsnoop.events():
var event rawExecEvent
var event commandDataT
err := unmarshalEvent(eventBytes, &event)
require.NoError(t, err)
// Since we're executing the command using the test binary,
// the arguments return on a single event, and the path of
// or command will come on the argv part.
argv := ConvertString(unsafe.Pointer(&event.Argv))
argv := ConvertString(event.Argv[:])
if event.Type == eventArg {
if test.expectPartialPath {
require.Len(t, argv, ArgvMax)
@@ -585,7 +484,6 @@ func TestRootLargeCommands(t *testing.T) {
}
})
}
}
// waitForEvent will wait for an event to arrive over the perf buffer and
@@ -613,7 +511,7 @@ func moveIntoCgroup(t *testing.T, pid int) (uint64, error) {
MountPath: cgroupPath,
})
if err != nil {
return 0, trace.Wrap(err)
return 0, trace.Wrap(err, "failed to mount cgroup")
}
t.Cleanup(func() {
const skipUnmount = false
@@ -624,13 +522,13 @@ func moveIntoCgroup(t *testing.T, pid int) (uint64, error) {
// Put the cmd in a new cgroup.
cgroupID, err := createCgroup(t, cgroupSrv, sessionID)
if err != nil {
return 0, trace.Wrap(err)
return 0, trace.Wrap(err, "failed to create cgroup")
}
// Place requested PID into cgroup.
err = cgroupSrv.Place(sessionID, pid)
if err != nil {
return 0, trace.Wrap(err)
return 0, trace.Wrap(err, "failed to place pid %d into cgroup", pid)
}
t.Cleanup(func() {
@@ -659,40 +557,25 @@ func createCgroup(t *testing.T, cgroup *cgroup.Service, sessionID string,
return cgroupID, nil
}
// executeCommand will execute some command in a loop.
func executeCommand(t *testing.T, doneContext context.Context, file string,
traceCgroup cgroupRegister,
) {
// executeCommand will execute some command.
func executeCommand(t *testing.T, file string, traceCgroup cgroupRegister) {
t.Helper()
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
fullPath, err := osexec.LookPath(file)
require.NoError(t, err, "Failed to find executable %q", file)
for {
select {
case <-ticker.C:
// Lookup and run the requested command.
path, err := osexec.LookPath(file)
if err != nil {
t.Logf("Failed to find executable %q: %v.", file, err)
}
fullPath, err := osexec.LookPath(path)
require.NoError(t, err)
runCmd(t, reexecInCGroupCmd, fullPath, traceCgroup, require.NoError)
case <-doneContext.Done():
return
}
}
err = runCmd(t, reexecInCGroupCmd, fullPath, traceCgroup)
require.NoError(t, err)
}
func runCmd(t *testing.T, reexecCmd string, arg string, traceCgroup cgroupRegister, cmdReturnAssertion require.ErrorAssertionFunc) {
func runCmd(t *testing.T, reexecCmd string, arg string, traceCgroup cgroupRegister) error {
t.Helper()
// Create a pipe to communicate with the child process after re-exec.
readP, writeP, err := os.Pipe()
require.NoError(t, err)
if err != nil {
return trace.Wrap(err, "failed to create pipe")
}
t.Cleanup(func() {
readP.Close()
@@ -701,60 +584,70 @@ func runCmd(t *testing.T, reexecCmd string, arg string, traceCgroup cgroupRegist
// Re-exec the test binary. We can then move the binary to a new cgroup.
cmd := osexec.Command(os.Args[0], reexecCmd, arg)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.ExtraFiles = append(cmd.ExtraFiles, readP)
// Start the re-exec
err = cmd.Start()
require.NoError(t, err)
if err != nil {
return trace.Wrap(err, "failed to start command")
}
cgroupID, err := moveIntoCgroup(t, cmd.Process.Pid)
require.NoError(t, err)
if err != nil {
return trace.Wrap(err, "failed to move pid %d into cgroup", cmd.Process.Pid)
}
// Register the process in the BPF module
err = traceCgroup.startSession(cgroupID)
require.NoError(t, err)
if err != nil {
return trace.Wrap(err, "failed to register cgroup in BPF module")
}
// Send one byte to continue the subprocess execution.
_, err = writeP.Write([]byte{1})
require.NoError(t, err)
if err != nil {
return trace.Wrap(err, "failed to write to pipe")
}
// Wait for the command to exit. Otherwise, we cannot clean up the cgroup.
cmdReturnAssertion(t, cmd.Wait())
waitErr := trace.Wrap(cmd.Wait())
// Remove the registered cgroup from the BPF module. Do not call it after
// BPF module is deregistered.
err = traceCgroup.endSession(cgroupID)
if err != nil {
return trace.NewAggregate(waitErr, trace.Wrap(err, "failed to deregister cgroup in BPF module"))
}
return waitErr
}
// executeHTTP will perform an HTTP GET to some endpoint in a subprocess
// that is placed into the traceCgroup cgroup so it can be tracked.
func executeHTTP(t *testing.T, endpoint string, ipv6 bool, traceCgroup cgroupRegister) {
t.Helper()
cmd := networkIPv4InCgroupCmd
if ipv6 {
cmd = networkIPv6InCgroupCmd
}
err := runCmd(t, cmd, endpoint, traceCgroup)
require.NoError(t, err)
}
// executeHTTP will perform a HTTP GET to some endpoint in a loop.
func executeHTTP(t *testing.T, doneContext context.Context, endpoint string, traceCgroup cgroupRegister) {
// checkBPF skips the test if BPF tests are not enabled or the test is not run
// as root.
func checkBPF(t *testing.T) {
t.Helper()
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Perform HTTP GET to the requested endpoint.
if _, err := http.Get(endpoint); err != nil {
t.Logf("HTTP request failed: %v.", err)
}
runCmd(t, networkInCgroupCmd, endpoint, traceCgroup, require.NoError)
case <-doneContext.Done():
return
}
if !bpfTestEnabled() {
t.Skip("BPF testing is disabled. Set TELEPORT_BPF_TEST environment variable to enable.")
}
if os.Geteuid() != 0 {
t.Skip("Tests for package bpf can only be run as root.")
}
}
// isRoot returns a boolean if the test is being run as root or not. Tests
// for this package must be run as root.
func isRoot() bool {
return os.Geteuid() == 0
}
// bpfTestEnabled returns true if BPF tests should run. Tests can be enabled by
-3
View File
@@ -1,3 +0,0 @@
## BPF Bytecode
After builds, this directory contains CO-RE BFP bytecode that is embedded within Teleport.
+126 -71
View File
@@ -21,9 +21,14 @@
package bpf
import (
_ "embed"
"context"
"io"
"sync"
"github.com/aquasecurity/libbpfgo"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
@@ -31,49 +36,56 @@ import (
"github.com/gravitational/teleport/lib/observability/metrics"
)
var (
lostCommandEvents = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricLostCommandEvents,
Help: "Number of lost command events.",
},
)
var lostCommandEvents = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricLostCommandEvents,
Help: "Number of lost command events.",
},
)
const (
commandEventsBuffer = "execve_events"
)
// rawExecEvent is sent by the eBPF program that Teleport pulls off the perf
// buffer.
type rawExecEvent struct {
// PID is the ID of the process.
PID uint64
// PPID is the PID of the parent process.
PPID uint64
// Command is the executable.
Command [CommMax]byte
// Type is the type of event.
Type int32
// Argv is the list of arguments to the program.
Argv [ArgvMax]byte
// ReturnCode is the return code of execve.
ReturnCode int32
// CgroupID is the internal cgroupv2 ID of the event.
CgroupID uint64
}
type exec struct {
session
// session
objs commandObjects
eventBuf *RingBuffer
lost *Counter
eventBuf *ringbuf.Reader
lost *ebpf.Map
toClose []io.Closer
closed bool
mtx sync.Mutex
bpfEvents chan []byte
lostCounter *Counter
}
func (e *exec) startSession(cgroupID uint64) error {
e.mtx.Lock()
defer e.mtx.Unlock()
if e.closed {
return trace.BadParameter("open session already closed")
}
if err := e.objs.MonitoredCgroups.Put(cgroupID, int64(0)); err != nil {
return trace.Wrap(err)
}
return nil
}
func (e *exec) endSession(cgroupID uint64) error {
e.mtx.Lock()
defer e.mtx.Unlock()
if e.closed {
return nil // Ignore. If the session is closed, the cgroup is no longer monitored.
}
if err := e.objs.MonitoredCgroups.Delete(&cgroupID); err != nil {
return trace.Wrap(err)
}
return nil
}
// startExec will load, start, and pull events off the ring buffer
@@ -84,59 +96,102 @@ func startExec(bufferSize int) (*exec, error) {
return nil, trace.Wrap(err)
}
e := &exec{}
// Remove resource limits for kernels <5.11.
if err := rlimit.RemoveMemlock(); err != nil {
return nil, trace.WrapWithMessage(err, "Removing memlock")
}
commandBPF, err := embedFS.ReadFile("bytecode/command.bpf.o")
var objs commandObjects
if err := loadCommandObjects(&objs, nil); err != nil {
return nil, trace.Wrap(err)
}
lostCtr, err := NewCounter(objs.LostCounter, objs.LostDoorbell, lostCommandEvents)
if err != nil {
return nil, trace.Wrap(err)
}
e.session.module, err = libbpfgo.NewModuleFromBuffer(commandBPF, "command")
if err != nil {
return nil, trace.Wrap(err)
toClose := make([]io.Closer, 0)
tracePoints := []struct {
name string
tracepoint *ebpf.Program
}{
{
name: "sys_enter_execve",
tracepoint: objs.TracepointSyscallsSysEnterExecve,
},
{
name: "sys_exit_execve",
tracepoint: objs.TracepointSyscallsSysExitExecve,
},
{
name: "sys_enter_execveat",
tracepoint: objs.TracepointSyscallsSysEnterExecveat,
},
{
name: "sys_exit_execveat",
tracepoint: objs.TracepointSyscallsSysExitExecveat,
},
}
// Resizing the ring buffer must be done here, after the module
// was created but before it's loaded into the kernel.
if err = ResizeMap(e.session.module, commandEventsBuffer, uint32(bufferSize*pageSize)); err != nil {
return nil, trace.Wrap(err)
}
// Load into the kernel
if err = e.session.module.BPFLoadObject(); err != nil {
return nil, trace.Wrap(err)
}
syscalls := []string{"execve", "execveat"}
for _, syscall := range syscalls {
if err = AttachSyscallTracepoint(e.session.module, syscall); err != nil {
for _, tp := range tracePoints {
tp, err := link.Tracepoint("syscalls", tp.name, tp.tracepoint, nil)
if err != nil {
return nil, trace.Wrap(err)
}
toClose = append(toClose, tp)
}
e.eventBuf, err = NewRingBuffer(e.session.module, commandEventsBuffer)
eventBuf, err := ringbuf.NewReader(objs.ExecveEvents)
if err != nil {
return nil, trace.Wrap(err)
}
e.lost, err = NewCounter(e.session.module, "lost", lostCommandEvents)
if err != nil {
return nil, trace.Wrap(err)
}
bpfEvents := make(chan []byte, bufferSize)
go sendEvents(bpfEvents, eventBuf)
return e, nil
return &exec{
objs: objs,
eventBuf: eventBuf,
lost: objs.LostCounter,
toClose: toClose,
bpfEvents: bpfEvents,
lostCounter: lostCtr,
}, nil
}
// close will stop reading events off the ring buffer and unload the BPF
// program. The ring buffer is closed as part of the module being closed.
func (e *exec) close() {
e.lost.Close()
e.eventBuf.Close()
e.session.module.Close()
e.mtx.Lock()
defer e.mtx.Unlock()
if e.closed {
return
}
e.closed = true
for _, link := range e.toClose {
if err := link.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close link", "error", err)
}
}
if err := e.objs.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close command objects", "error", err)
}
if err := e.lostCounter.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close command lost counter", "error", err)
}
logger.DebugContext(context.Background(), "Closed command BPF module")
}
// events contains raw events off the perf buffer.
func (e *exec) events() <-chan []byte {
return e.eventBuf.EventCh
return e.bpfEvents
}
+165
View File
@@ -0,0 +1,165 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64 && bpf
package bpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"structs"
"github.com/cilium/ebpf"
)
type commandDataT struct {
_ structs.HostLayout
Pid uint64
Ppid uint64
Command [16]uint8
Type uint32
Argv [1024]uint8
Retval int32
Cgroup uint64
}
// loadCommand returns the embedded CollectionSpec for command.
func loadCommand() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_CommandBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load command: %w", err)
}
return spec, err
}
// loadCommandObjects loads command and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *commandObjects
// *commandPrograms
// *commandMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadCommandObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadCommand()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// commandSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandSpecs struct {
commandProgramSpecs
commandMapSpecs
commandVariableSpecs
}
// commandProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandProgramSpecs struct {
TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"`
TracepointSyscallsSysEnterExecveat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execveat"`
TracepointSyscallsSysExitExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_execve"`
TracepointSyscallsSysExitExecveat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_execveat"`
}
// commandMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandMapSpecs struct {
ExecveEvents *ebpf.MapSpec `ebpf:"execve_events"`
LostCounter *ebpf.MapSpec `ebpf:"lost_counter"`
LostDoorbell *ebpf.MapSpec `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.MapSpec `ebpf:"monitored_cgroups"`
}
// commandVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandVariableSpecs struct {
Unused *ebpf.VariableSpec `ebpf:"unused"`
}
// commandObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandObjects struct {
commandPrograms
commandMaps
commandVariables
}
func (o *commandObjects) Close() error {
return _CommandClose(
&o.commandPrograms,
&o.commandMaps,
)
}
// commandMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandMaps struct {
ExecveEvents *ebpf.Map `ebpf:"execve_events"`
LostCounter *ebpf.Map `ebpf:"lost_counter"`
LostDoorbell *ebpf.Map `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.Map `ebpf:"monitored_cgroups"`
}
func (m *commandMaps) Close() error {
return _CommandClose(
m.ExecveEvents,
m.LostCounter,
m.LostDoorbell,
m.MonitoredCgroups,
)
}
// commandVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandVariables struct {
Unused *ebpf.Variable `ebpf:"unused"`
}
// commandPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandPrograms struct {
TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"`
TracepointSyscallsSysEnterExecveat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execveat"`
TracepointSyscallsSysExitExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_execve"`
TracepointSyscallsSysExitExecveat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_execveat"`
}
func (p *commandPrograms) Close() error {
return _CommandClose(
p.TracepointSyscallsSysEnterExecve,
p.TracepointSyscallsSysEnterExecveat,
p.TracepointSyscallsSysExitExecve,
p.TracepointSyscallsSysExitExecveat,
)
}
func _CommandClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed command_arm64_bpfel.o
var _CommandBytes []byte
Binary file not shown.
+165
View File
@@ -0,0 +1,165 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build (386 || amd64) && bpf
package bpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"structs"
"github.com/cilium/ebpf"
)
type commandDataT struct {
_ structs.HostLayout
Pid uint64
Ppid uint64
Command [16]uint8
Type uint32
Argv [1024]uint8
Retval int32
Cgroup uint64
}
// loadCommand returns the embedded CollectionSpec for command.
func loadCommand() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_CommandBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load command: %w", err)
}
return spec, err
}
// loadCommandObjects loads command and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *commandObjects
// *commandPrograms
// *commandMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadCommandObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadCommand()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// commandSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandSpecs struct {
commandProgramSpecs
commandMapSpecs
commandVariableSpecs
}
// commandProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandProgramSpecs struct {
TracepointSyscallsSysEnterExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execve"`
TracepointSyscallsSysEnterExecveat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_execveat"`
TracepointSyscallsSysExitExecve *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_execve"`
TracepointSyscallsSysExitExecveat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_execveat"`
}
// commandMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandMapSpecs struct {
ExecveEvents *ebpf.MapSpec `ebpf:"execve_events"`
LostCounter *ebpf.MapSpec `ebpf:"lost_counter"`
LostDoorbell *ebpf.MapSpec `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.MapSpec `ebpf:"monitored_cgroups"`
}
// commandVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type commandVariableSpecs struct {
Unused *ebpf.VariableSpec `ebpf:"unused"`
}
// commandObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandObjects struct {
commandPrograms
commandMaps
commandVariables
}
func (o *commandObjects) Close() error {
return _CommandClose(
&o.commandPrograms,
&o.commandMaps,
)
}
// commandMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandMaps struct {
ExecveEvents *ebpf.Map `ebpf:"execve_events"`
LostCounter *ebpf.Map `ebpf:"lost_counter"`
LostDoorbell *ebpf.Map `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.Map `ebpf:"monitored_cgroups"`
}
func (m *commandMaps) Close() error {
return _CommandClose(
m.ExecveEvents,
m.LostCounter,
m.LostDoorbell,
m.MonitoredCgroups,
)
}
// commandVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandVariables struct {
Unused *ebpf.Variable `ebpf:"unused"`
}
// commandPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadCommandObjects or ebpf.CollectionSpec.LoadAndAssign.
type commandPrograms struct {
TracepointSyscallsSysEnterExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execve"`
TracepointSyscallsSysEnterExecveat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_execveat"`
TracepointSyscallsSysExitExecve *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_execve"`
TracepointSyscallsSysExitExecveat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_execveat"`
}
func (p *commandPrograms) Close() error {
return _CommandClose(
p.TracepointSyscallsSysEnterExecve,
p.TracepointSyscallsSysEnterExecveat,
p.TracepointSyscallsSysExitExecve,
p.TracepointSyscallsSysExitExecveat,
)
}
func _CommandClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed command_x86_bpfel.o
var _CommandBytes []byte
Binary file not shown.
-65
View File
@@ -1,65 +0,0 @@
//go:build bpf && !386
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package bpf
import (
"unsafe"
"github.com/aquasecurity/libbpfgo"
"github.com/gravitational/trace"
)
const monitoredCGroups = "monitored_cgroups"
type session struct {
module *libbpfgo.Module
}
// startSession registers the given cgroup in the BPF module. Only registered
// cgroups will return events to the userspace.
func (s *session) startSession(cgroupID uint64) error {
cgroupMap, err := s.module.GetMap(monitoredCGroups)
if err != nil {
return trace.Wrap(err)
}
dummyVal := 0
err = cgroupMap.Update(unsafe.Pointer(&cgroupID), unsafe.Pointer(&dummyVal))
if err != nil {
return trace.Wrap(err)
}
return nil
}
// endSession removes the previously registered cgroup from the BPF module.
func (s *session) endSession(cgroupID uint64) error {
cgroupMap, err := s.module.GetMap(monitoredCGroups)
if err != nil {
return trace.Wrap(err)
}
if err := cgroupMap.DeleteKey(unsafe.Pointer(&cgroupID)); err != nil {
return trace.Wrap(err)
}
return nil
}
+3 -3
View File
@@ -36,7 +36,7 @@ func TestBPFConfig_CheckAndSetDefaults(t *testing.T) {
openPerfBufferPageCount := defaults.OpenPerfBufferPageCount
zeroPageCount := 0
var tests = []struct {
tests := []struct {
name string
got *servicecfg.BPFConfig
want *servicecfg.BPFConfig
@@ -60,8 +60,8 @@ func TestBPFConfig_CheckAndSetDefaults(t *testing.T) {
CgroupPath: "/my/cgroup/",
},
want: &servicecfg.BPFConfig{
CommandBufferSize: &zeroPageCount,
DiskBufferSize: &zeroPageCount,
CommandBufferSize: &perfBufferPageCount,
DiskBufferSize: &openPerfBufferPageCount,
NetworkBufferSize: &perfBufferPageCount,
CgroupPath: "/my/cgroup/",
},
+144 -76
View File
@@ -21,10 +21,15 @@
package bpf
import (
_ "embed"
"context"
"io"
"runtime"
"sync"
"github.com/aquasecurity/libbpfgo"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
@@ -32,51 +37,23 @@ import (
"github.com/gravitational/teleport/lib/observability/metrics"
)
var (
lostDiskEvents = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricLostDiskEvents,
Help: "Number of lost disk events.",
},
)
var lostDiskEvents = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricLostDiskEvents,
Help: "Number of lost disk events.",
},
)
const (
diskEventsBuffer = "open_events"
)
// rawOpenEvent is sent by the eBPF program that Teleport pulls off the perf
// buffer.
type rawOpenEvent struct {
// CgroupID is the internal cgroupv2 ID of the event.
CgroupID uint64
// PID is the ID of the process.
PID uint64
// ReturnCode is the return code of open.
ReturnCode int32
// Command is name of the executable opening the file.
Command [CommMax]byte
// Path is the full path to the file being opened.
Path [PathMax]byte
// Flags are the flags passed to open.
Flags int32
}
type cgroupRegister interface {
startSession(cgroupID uint64) error
endSession(cgroupID uint64) error
}
type open struct {
session
objs diskObjects
eventBuf *RingBuffer
lost *Counter
eventBuf chan []byte
toClose []io.Closer
closed bool
mtx sync.Mutex
lostCounter *Counter
}
// startOpen will compile, load, start, and pull events off the perf buffer
@@ -84,67 +61,158 @@ type open struct {
func startOpen(bufferSize int) (*open, error) {
err := metrics.RegisterPrometheusCollectors(lostDiskEvents)
if err != nil {
return nil, trace.Wrap(err, "registering prometheus collectors: %v", err)
}
// Remove resource limits for kernels <5.11.
if err := rlimit.RemoveMemlock(); err != nil {
logger.ErrorContext(context.Background(), "Removing memlock failed", "error", err)
return nil, trace.Wrap(err)
}
o := &open{}
var objs diskObjects
if err := loadDiskObjects(&objs, nil); err != nil {
return nil, trace.Wrap(err, "loading disk objects: %v", err)
}
diskBPF, err := embedFS.ReadFile("bytecode/disk.bpf.o")
lostCtr, err := NewCounter(objs.LostCounter, objs.LostDoorbell, lostDiskEvents)
if err != nil {
return nil, trace.Wrap(err)
}
o.session.module, err = libbpfgo.NewModuleFromBuffer(diskBPF, "disk")
if err != nil {
return nil, trace.Wrap(err)
trs := []struct {
name string
prog *ebpf.Program
}{
{
name: "sys_enter_openat",
prog: objs.TracepointSyscallsSysEnterOpenat,
},
{
name: "sys_exit_openat",
prog: objs.TracepointSyscallsSysExitOpenat,
},
{
name: "sys_enter_openat2",
prog: objs.TracepointSyscallsSysEnterOpenat2,
},
{
name: "sys_exit_openat2",
prog: objs.TracepointSyscallsSysExitOpenat2,
},
}
// Resizing the ring buffer must be done here, after the module
// was created but before it's loaded into the kernel.
if err = ResizeMap(o.session.module, diskEventsBuffer, uint32(bufferSize*pageSize)); err != nil {
return nil, trace.Wrap(err)
}
// Load into the kernel
if err = o.session.module.BPFLoadObject(); err != nil {
return nil, trace.Wrap(err)
}
syscalls := []string{"openat", "openat2"}
if runtime.GOARCH != "arm64" {
// open is not implemented on arm64.
syscalls = append(syscalls, "open")
// creat and open are not implemented on arm64.
trs = append(trs, []struct {
name string
prog *ebpf.Program
}{
{
name: "sys_enter_creat",
prog: objs.TracepointSyscallsSysEnterCreat,
},
{
name: "sys_exit_creat",
prog: objs.TracepointSyscallsSysExitCreat,
},
{
name: "sys_enter_open",
prog: objs.TracepointSyscallsSysEnterOpen,
},
{
name: "sys_exit_open",
prog: objs.TracepointSyscallsSysExitOpen,
},
}...)
}
for _, syscall := range syscalls {
if err = AttachSyscallTracepoint(o.session.module, syscall); err != nil {
return nil, trace.Wrap(err)
toClose := make([]io.Closer, 0, len(trs))
for _, tr := range trs {
tp, err := link.Tracepoint("syscalls", tr.name, tr.prog, nil)
if err != nil {
return nil, trace.Wrap(err, "linking %q tracepoint: %v", tr.name, err)
}
toClose = append(toClose, tp)
}
o.eventBuf, err = NewRingBuffer(o.session.module, diskEventsBuffer)
eventBuf, err := ringbuf.NewReader(objs.OpenEvents)
if err != nil {
return nil, trace.Wrap(err)
return nil, trace.Wrap(err, "creating ring buffer reader: %v", err)
}
o.lost, err = NewCounter(o.session.module, "lost", lostDiskEvents)
if err != nil {
return nil, trace.Wrap(err)
bpfEvents := make(chan []byte, bufferSize)
go sendEvents(bpfEvents, eventBuf)
return &open{
objs: objs,
eventBuf: bpfEvents,
toClose: toClose,
lostCounter: lostCtr,
}, nil
}
func (o *open) startSession(cgroupID uint64) error {
o.mtx.Lock()
defer o.mtx.Unlock()
if o.closed {
return trace.BadParameter("open session already closed")
}
return o, nil
if err := o.objs.MonitoredCgroups.Put(cgroupID, int64(0)); err != nil {
return trace.Wrap(err)
}
return nil
}
func (o *open) endSession(cgroupID uint64) error {
o.mtx.Lock()
defer o.mtx.Unlock()
if o.closed {
return nil // Ignore. If the session is closed, the cgroup is no longer monitored.
}
if err := o.objs.MonitoredCgroups.Delete(&cgroupID); err != nil {
return trace.Wrap(err)
}
return nil
}
// close will stop reading events off the ring buffer and unload the BPF
// program. The ring buffer is closed as part of the module being closed.
func (o *open) close() {
o.lost.Close()
o.eventBuf.Close()
o.session.module.Close()
o.mtx.Lock()
defer o.mtx.Unlock()
if o.closed {
return
}
o.closed = true
for _, toClose := range o.toClose {
if err := toClose.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close link", "error", err)
}
}
if err := o.objs.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close disk objects", "error", err)
}
if err := o.lostCounter.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close disk lost counter", "error", err)
}
logger.DebugContext(context.Background(), "Closed disk BPF module")
}
// events contains raw events off the perf buffer.
func (o *open) events() <-chan []byte {
return o.eventBuf.EventCh
return o.eventBuf
}
+180
View File
@@ -0,0 +1,180 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64 && bpf
package bpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"structs"
"github.com/cilium/ebpf"
)
type diskDataT struct {
_ structs.HostLayout
Cgroup uint64
Pid uint64
ReturnCode int32
Command [16]uint8
FilePath [255]uint8
_ [1]byte
Flags int32
}
// loadDisk returns the embedded CollectionSpec for disk.
func loadDisk() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_DiskBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load disk: %w", err)
}
return spec, err
}
// loadDiskObjects loads disk and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *diskObjects
// *diskPrograms
// *diskMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadDiskObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadDisk()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// diskSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskSpecs struct {
diskProgramSpecs
diskMapSpecs
diskVariableSpecs
}
// diskProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskProgramSpecs struct {
TracepointSyscallsSysEnterCreat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_creat"`
TracepointSyscallsSysEnterOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_open"`
TracepointSyscallsSysEnterOpenat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_openat"`
TracepointSyscallsSysEnterOpenat2 *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_openat2"`
TracepointSyscallsSysExitCreat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_creat"`
TracepointSyscallsSysExitOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_open"`
TracepointSyscallsSysExitOpenat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_openat"`
TracepointSyscallsSysExitOpenat2 *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_openat2"`
}
// diskMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskMapSpecs struct {
Infotmp *ebpf.MapSpec `ebpf:"infotmp"`
LostCounter *ebpf.MapSpec `ebpf:"lost_counter"`
LostDoorbell *ebpf.MapSpec `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.MapSpec `ebpf:"monitored_cgroups"`
OpenEvents *ebpf.MapSpec `ebpf:"open_events"`
}
// diskVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskVariableSpecs struct {
Unused *ebpf.VariableSpec `ebpf:"unused"`
}
// diskObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskObjects struct {
diskPrograms
diskMaps
diskVariables
}
func (o *diskObjects) Close() error {
return _DiskClose(
&o.diskPrograms,
&o.diskMaps,
)
}
// diskMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskMaps struct {
Infotmp *ebpf.Map `ebpf:"infotmp"`
LostCounter *ebpf.Map `ebpf:"lost_counter"`
LostDoorbell *ebpf.Map `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.Map `ebpf:"monitored_cgroups"`
OpenEvents *ebpf.Map `ebpf:"open_events"`
}
func (m *diskMaps) Close() error {
return _DiskClose(
m.Infotmp,
m.LostCounter,
m.LostDoorbell,
m.MonitoredCgroups,
m.OpenEvents,
)
}
// diskVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskVariables struct {
Unused *ebpf.Variable `ebpf:"unused"`
}
// diskPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskPrograms struct {
TracepointSyscallsSysEnterCreat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_creat"`
TracepointSyscallsSysEnterOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_open"`
TracepointSyscallsSysEnterOpenat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_openat"`
TracepointSyscallsSysEnterOpenat2 *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_openat2"`
TracepointSyscallsSysExitCreat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_creat"`
TracepointSyscallsSysExitOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_open"`
TracepointSyscallsSysExitOpenat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_openat"`
TracepointSyscallsSysExitOpenat2 *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_openat2"`
}
func (p *diskPrograms) Close() error {
return _DiskClose(
p.TracepointSyscallsSysEnterCreat,
p.TracepointSyscallsSysEnterOpen,
p.TracepointSyscallsSysEnterOpenat,
p.TracepointSyscallsSysEnterOpenat2,
p.TracepointSyscallsSysExitCreat,
p.TracepointSyscallsSysExitOpen,
p.TracepointSyscallsSysExitOpenat,
p.TracepointSyscallsSysExitOpenat2,
)
}
func _DiskClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed disk_arm64_bpfel.o
var _DiskBytes []byte
Binary file not shown.
+180
View File
@@ -0,0 +1,180 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build (386 || amd64) && bpf
package bpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"structs"
"github.com/cilium/ebpf"
)
type diskDataT struct {
_ structs.HostLayout
Cgroup uint64
Pid uint64
ReturnCode int32
Command [16]uint8
FilePath [255]uint8
_ [1]byte
Flags int32
}
// loadDisk returns the embedded CollectionSpec for disk.
func loadDisk() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_DiskBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load disk: %w", err)
}
return spec, err
}
// loadDiskObjects loads disk and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *diskObjects
// *diskPrograms
// *diskMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadDiskObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadDisk()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// diskSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskSpecs struct {
diskProgramSpecs
diskMapSpecs
diskVariableSpecs
}
// diskProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskProgramSpecs struct {
TracepointSyscallsSysEnterCreat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_creat"`
TracepointSyscallsSysEnterOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_open"`
TracepointSyscallsSysEnterOpenat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_openat"`
TracepointSyscallsSysEnterOpenat2 *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_enter_openat2"`
TracepointSyscallsSysExitCreat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_creat"`
TracepointSyscallsSysExitOpen *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_open"`
TracepointSyscallsSysExitOpenat *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_openat"`
TracepointSyscallsSysExitOpenat2 *ebpf.ProgramSpec `ebpf:"tracepoint__syscalls__sys_exit_openat2"`
}
// diskMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskMapSpecs struct {
Infotmp *ebpf.MapSpec `ebpf:"infotmp"`
LostCounter *ebpf.MapSpec `ebpf:"lost_counter"`
LostDoorbell *ebpf.MapSpec `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.MapSpec `ebpf:"monitored_cgroups"`
OpenEvents *ebpf.MapSpec `ebpf:"open_events"`
}
// diskVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type diskVariableSpecs struct {
Unused *ebpf.VariableSpec `ebpf:"unused"`
}
// diskObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskObjects struct {
diskPrograms
diskMaps
diskVariables
}
func (o *diskObjects) Close() error {
return _DiskClose(
&o.diskPrograms,
&o.diskMaps,
)
}
// diskMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskMaps struct {
Infotmp *ebpf.Map `ebpf:"infotmp"`
LostCounter *ebpf.Map `ebpf:"lost_counter"`
LostDoorbell *ebpf.Map `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.Map `ebpf:"monitored_cgroups"`
OpenEvents *ebpf.Map `ebpf:"open_events"`
}
func (m *diskMaps) Close() error {
return _DiskClose(
m.Infotmp,
m.LostCounter,
m.LostDoorbell,
m.MonitoredCgroups,
m.OpenEvents,
)
}
// diskVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskVariables struct {
Unused *ebpf.Variable `ebpf:"unused"`
}
// diskPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadDiskObjects or ebpf.CollectionSpec.LoadAndAssign.
type diskPrograms struct {
TracepointSyscallsSysEnterCreat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_creat"`
TracepointSyscallsSysEnterOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_open"`
TracepointSyscallsSysEnterOpenat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_openat"`
TracepointSyscallsSysEnterOpenat2 *ebpf.Program `ebpf:"tracepoint__syscalls__sys_enter_openat2"`
TracepointSyscallsSysExitCreat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_creat"`
TracepointSyscallsSysExitOpen *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_open"`
TracepointSyscallsSysExitOpenat *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_openat"`
TracepointSyscallsSysExitOpenat2 *ebpf.Program `ebpf:"tracepoint__syscalls__sys_exit_openat2"`
}
func (p *diskPrograms) Close() error {
return _DiskClose(
p.TracepointSyscallsSysEnterCreat,
p.TracepointSyscallsSysEnterOpen,
p.TracepointSyscallsSysEnterOpenat,
p.TracepointSyscallsSysEnterOpenat2,
p.TracepointSyscallsSysExitCreat,
p.TracepointSyscallsSysExitOpen,
p.TracepointSyscallsSysExitOpenat,
p.TracepointSyscallsSysExitOpenat2,
)
}
func _DiskClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed disk_x86_bpfel.o
var _DiskBytes []byte
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
/*
* Teleport
* Copyright (C) 2025 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package bpf
// Multi-arch setup, as mentioned in https://github.com/cilium/ebpf/issues/305
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 -cflags "-D__TARGET_ARCH_x86" -tags bpf -type data_t command ../../bpf/enhancedrecording/command.bpf.c
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 -cflags "-D__TARGET_ARCH_x86" -tags bpf -type data_t -no-global-types disk ../../bpf/enhancedrecording/disk.bpf.c
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 -cflags "-D__TARGET_ARCH_x86" -tags bpf -type ipv4_data_t -type ipv6_data_t network ../../bpf/enhancedrecording/network.bpf.c
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 -cflags "-D__TARGET_ARCH_arm64" -tags bpf -type data_t command ../../bpf/enhancedrecording/command.bpf.c
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 -cflags "-D__TARGET_ARCH_arm64" -tags bpf -no-global-types -type data_t disk ../../bpf/enhancedrecording/disk.bpf.c
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 -cflags "-D__TARGET_ARCH_arm64" -tags bpf -type ipv4_data_t -type ipv6_data_t network ../../bpf/enhancedrecording/network.bpf.c
+73 -207
View File
@@ -22,12 +22,11 @@ package bpf
import (
"context"
"encoding/binary"
"os"
"errors"
"sync"
"unsafe"
"github.com/aquasecurity/libbpfgo"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/ringbuf"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
@@ -37,206 +36,6 @@ import (
var logger = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentBPF)
const (
kprobeProgPrefix = "kprobe__"
kretprobeProgPrefix = "kretprobe__"
tracepointProgPrefix = "tracepoint__"
syscallsCategory = "syscalls"
syscallEnterPrefix = "sys_enter_"
syscallExitPrefix = "sys_exit_"
counterSuffix = "_counter"
doorbellSuffix = "_doorbell"
)
var pageSize = os.Getpagesize()
// ResizeMap resizes (changes max number of entries) the
// map to the specified value. This function must be called
// before BPFLoadObject has been called.
func ResizeMap(mod *libbpfgo.Module, mapName string, value uint32) error {
m, err := mod.GetMap(mapName)
if err != nil {
return trace.Wrap(err)
}
if err = m.Resize(value); err != nil {
return trace.Wrap(err)
}
return nil
}
// AttachKprobe attaches both a kprobe and kretprobe for the
// function identified by "name". The BPF C functions must be
// called "kprobe__NAME" and "kretprobe__NAME" where NAME
// the name of the kernel function to be hooked.
func AttachKprobe(mod *libbpfgo.Module, name string) error {
prog, err := mod.GetProgram(kprobeProgPrefix + name)
if err != nil {
return trace.Wrap(err)
}
_, err = prog.AttachKprobe(name)
if err != nil {
return trace.Wrap(err)
}
prog, err = mod.GetProgram(kretprobeProgPrefix + name)
if err != nil {
return trace.Wrap(err)
}
_, err = prog.AttachKretprobe(name)
if err != nil {
return trace.Wrap(err)
}
return nil
}
// AttachTracepoint attaches a tracepoint identified by category:name.
// The BPF C function must be called "tracepoint__CATEGORY__NAME" where
// CATEGORY and NAME are the tracepoint's category and name.
func AttachTracepoint(mod *libbpfgo.Module, category, name string) error {
prog, err := mod.GetProgram(tracepointProgPrefix + category + "__" + name)
if err != nil {
return trace.Wrap(err)
}
_, err = prog.AttachTracepoint(category, name)
return err
}
// AttachSyscallTracepoint hooks a syscall using the tracepoint mechanism.
// Like Kprobe, it hooks the entry and exit points. Unlike the Kprobe, it
// is stable and not sensitive to the kernel renaming its functions.
// The BPF C functions must be called "tracepoint__sys_enter__SYSCALL" and
// "tracepoint__sys_exit__SYSCALL" where SYSCALL is the name of the syscalled
// to be hooked.
//
// For more details, see https://www.kernel.org/doc/html/v5.8/trace/events.html.
func AttachSyscallTracepoint(mod *libbpfgo.Module, syscall string) error {
if err := AttachTracepoint(mod, syscallsCategory, syscallEnterPrefix+syscall); err != nil {
return trace.Wrap(err)
}
if err := AttachTracepoint(mod, syscallsCategory, syscallExitPrefix+syscall); err != nil {
return trace.Wrap(err)
}
return nil
}
// RingBuffer wraps a BPF ring buffer with a channel that will
// receive the data from the ring buffer.
type RingBuffer struct {
buf *libbpfgo.RingBuffer
EventCh chan []byte
}
// NewRingBuffer creates a RingBuffer object identified by "name". The messages
// from the ring buffer will be available on the EventCh channel.
func NewRingBuffer(mod *libbpfgo.Module, name string) (*RingBuffer, error) {
rb := &RingBuffer{
EventCh: make(chan []byte, chanSize),
}
var err error
rb.buf, err = mod.InitRingBuf(name, rb.EventCh)
if err != nil {
return nil, trace.Wrap(err)
}
rb.buf.Start()
return rb, nil
}
// Close will stop receiving messages from the kernel and putting them
// into EventCh channel.
func (rb *RingBuffer) Close() {
rb.buf.Stop()
// don't Close ring buffers as they'll be closed as part of Module.Close()
}
// Counter allows a BPF program to increment a Prometheus counter.
// The counter value is stored in a one element BPF array (map).
// When it's incremented, the BPF program also rings the doorbell
// via a ring buffer.
type Counter struct {
// doorbellBuf contains dummy bytes and is used for signaling the userspace
doorbellBuf *libbpfgo.RingBuffer
// doorbellCh is the chan corresponding to doorbellBuf
doorbellCh chan []byte
// arr is a one element array containing the value
arr *libbpfgo.BPFMap
// lastCnt keeps the last read counter value
lastCnt uint64
// wg is used to wait for the loop goroutine to finish
wg sync.WaitGroup
// counter is the associated Prometheus counter to increment
counter prometheus.Counter
}
// NewCounter starts tracking the lost messages and updating the Prometheus counter.
func NewCounter(mod *libbpfgo.Module, name string, counter prometheus.Counter) (*Counter, error) {
c := &Counter{
doorbellCh: make(chan []byte, chanSize),
counter: counter,
}
var err error
c.arr, err = mod.GetMap(name + counterSuffix)
if err != nil {
return nil, trace.Wrap(err)
}
c.doorbellBuf, err = mod.InitRingBuf(name+doorbellSuffix, c.doorbellCh)
if err != nil {
return nil, trace.Wrap(err)
}
c.doorbellBuf.Start()
c.wg.Add(1)
go c.loop()
return c, nil
}
// Close will stop tracking and release the resources.
func (c *Counter) Close() {
c.doorbellBuf.Stop()
// don't Close ring buffers as they'll be closed as part of Module.Close()
// wait for lostLoop to finish
c.wg.Wait()
}
func (c *Counter) loop() {
for range c.doorbellCh {
var key int32 = 0
cntBytes, err := c.arr.GetValue(unsafe.Pointer(&key))
if err != nil {
logger.ErrorContext(context.Background(), "Error reading array value at index 0")
continue
}
cnt := binary.LittleEndian.Uint64(cntBytes)
if delta := cnt - c.lastCnt; delta > 0 {
c.counter.Add(float64(delta))
}
c.lastCnt = cnt
}
c.wg.Done()
}
const (
// CommMax is the maximum length of a command from linux/sched.h.
CommMax = 16
@@ -252,7 +51,74 @@ const (
// eventRet holds the return value and other data about an event.
eventRet = 1
// chanSize is the size of the event channels.
chanSize = 1024
)
// Counter allows a BPF program to increment a Prometheus counter.
// The counter value is stored in a one element BPF array (map).
// When it's incremented, the BPF program also rings the doorbell
// via a ring buffer.
type Counter struct {
// doorbellBuf contains dummy bytes and is used for signaling the userspace
doorbellBuf *ringbuf.Reader
// arr is a one element array containing the value
arr *ebpf.Map
// lastCnt keeps the last read counter value
lastCnt uint64
// wg is used to wait for the loop goroutine to finish
wg sync.WaitGroup
// counter is the associated Prometheus counter to increment
counter prometheus.Counter
}
// NewCounter starts tracking the lost messages and updating the Prometheus counter.
func NewCounter(counter, doorbell *ebpf.Map, promCounter prometheus.Counter) (*Counter, error) {
doorbellBuf, err := ringbuf.NewReader(doorbell)
if err != nil {
return nil, trace.Wrap(err)
}
c := &Counter{
doorbellBuf: doorbellBuf,
arr: counter,
counter: promCounter,
}
c.wg.Go(c.loop)
return c, nil
}
// Close will stop tracking and release the resources.
func (c *Counter) Close() error {
err := c.doorbellBuf.Close()
// wait for lostLoop to finish
c.wg.Wait()
return err
}
func (c *Counter) loop() {
for {
_, err := c.doorbellBuf.Read()
if err != nil {
if errors.Is(err, ringbuf.ErrClosed) {
return
}
logger.ErrorContext(context.Background(), "Error reading from ring buffer", "error", err)
return
}
var key int32 = 0
var count uint64
if err := c.arr.Lookup(&key, &count); err != nil {
logger.ErrorContext(context.Background(), "Error reading array value at index 0", "error", err)
continue
}
if delta := count - c.lastCnt; delta > 0 {
c.counter.Add(float64(delta))
}
c.lastCnt = count
}
}
+144 -108
View File
@@ -21,9 +21,14 @@
package bpf
import (
_ "embed"
"context"
"io"
"sync"
"github.com/aquasecurity/libbpfgo"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
"github.com/gravitational/trace"
"github.com/prometheus/client_golang/prometheus"
@@ -31,77 +36,24 @@ import (
"github.com/gravitational/teleport/lib/observability/metrics"
)
var (
lostNetworkEvents = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricLostNetworkEvents,
Help: "Number of lost network events.",
},
)
var lostNetworkEvents = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricLostNetworkEvents,
Help: "Number of lost network events.",
},
)
const (
network4EventsBuffer = "ipv4_events"
network6EventsBuffer = "ipv6_events"
)
// rawConn4Event is sent by the eBPF program that Teleport pulls off the perf
// buffer.
type rawConn4Event struct {
// CgroupID is the internal cgroupv2 ID of the event.
CgroupID uint64
// Version is the version of TCP (4 or 6).
Version uint64
// PID is the process ID.
PID uint32
// SrcAddr is the source IP address.
SrcAddr uint32
// DstAddr is the destination IP address.
DstAddr uint32
// DstPort is the port the connection is being made to.
DstPort uint16
// Command is name of the executable making the connection.
Command [CommMax]byte
}
// rawConn6Event is sent by the eBPF program that Teleport pulls off the perf
// buffer.
type rawConn6Event struct {
// CgroupID is the internal cgroupv2 ID of the event.
CgroupID uint64
// Version is the version of TCP (4 or 6).
Version uint64
// PID is the process ID.
PID uint32
// SrcAddr is the source IP address.
SrcAddr [4]uint32
// DstAddr is the destination IP address.
DstAddr [4]uint32
// DstPort is the port the connection is being made to.
DstPort uint16
// Command is name of the executable making the connection.
Command [CommMax]byte
}
type conn struct {
session
objs *networkObjects
event4Buf *RingBuffer
event6Buf *RingBuffer
event4Chan chan []byte
event6Chan chan []byte
toClose []io.Closer
lost *Counter
closed bool
mtx sync.Mutex
lostCounter *Counter
}
func startConn(bufferSize int) (*conn, error) {
@@ -110,74 +62,158 @@ func startConn(bufferSize int) (*conn, error) {
return nil, trace.Wrap(err)
}
c := &conn{}
// Remove resource limits for kernels <5.11.
if err := rlimit.RemoveMemlock(); err != nil {
return nil, trace.WrapWithMessage(err, "Removing memlock")
}
networkBPF, err := embedFS.ReadFile("bytecode/network.bpf.o")
var objs networkObjects
if err := loadNetworkObjects(&objs, nil); err != nil {
return nil, trace.Wrap(err)
}
lostCtr, err := NewCounter(objs.LostCounter, objs.LostDoorbell, lostNetworkEvents)
if err != nil {
return nil, trace.Wrap(err)
}
c.session.module, err = libbpfgo.NewModuleFromBuffer(networkBPF, "network")
kprobes := []struct {
symbol string
prog *ebpf.Program
}{
{
symbol: "tcp_v4_connect",
prog: objs.KprobeTcpV4Connect,
},
{
symbol: "tcp_v6_connect",
prog: objs.KprobeTcpV6Connect,
},
}
kretProbes := []struct {
symbol string
prog *ebpf.Program
}{
{
symbol: "tcp_v4_connect",
prog: objs.KretprobeTcpV4Connect,
},
{
symbol: "tcp_v6_connect",
prog: objs.KretprobeTcpV6Connect,
},
}
toClose := make([]io.Closer, 0)
for _, kprobe := range kprobes {
kp, err := link.Kprobe(kprobe.symbol, kprobe.prog, nil)
if err != nil {
return nil, trace.Wrap(err)
}
toClose = append(toClose, kp)
}
for _, kretprobe := range kretProbes {
kret, err := link.Kretprobe(kretprobe.symbol, kretprobe.prog, nil)
if err != nil {
return nil, trace.Wrap(err)
}
toClose = append(toClose, kret)
}
eventBufV4, err := ringbuf.NewReader(objs.Ipv4Events)
if err != nil {
return nil, trace.Wrap(err)
}
eventBufV6, err := ringbuf.NewReader(objs.Ipv6Events)
if err != nil {
return nil, trace.Wrap(err)
}
// Resizing the ring buffer must be done here, after the module
// was created but before it's loaded into the kernel.
if err = ResizeMap(c.session.module, network4EventsBuffer, uint32(bufferSize*pageSize)); err != nil {
return nil, trace.Wrap(err)
bpfv4Events := make(chan []byte, bufferSize)
go sendEvents(bpfv4Events, eventBufV4)
bpfv6Events := make(chan []byte, bufferSize)
go sendEvents(bpfv6Events, eventBufV6)
return &conn{
objs: &objs,
event4Chan: bpfv4Events,
event6Chan: bpfv6Events,
toClose: toClose,
lostCounter: lostCtr,
}, nil
}
func (c *conn) startSession(cgroupID uint64) error {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.closed {
return trace.BadParameter("connection is closed")
}
if err = ResizeMap(c.session.module, network6EventsBuffer, uint32(bufferSize*pageSize)); err != nil {
return nil, trace.Wrap(err)
if err := c.objs.MonitoredCgroups.Put(cgroupID, int64(0)); err != nil {
return trace.Wrap(err)
}
// Load into the kernel
if err = c.session.module.BPFLoadObject(); err != nil {
return nil, trace.Wrap(err)
return nil
}
func (c *conn) endSession(cgroupID uint64) error {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.closed {
return nil // Ignore. If the session is closed, the cgroup is no longer monitored.
}
if err = AttachKprobe(c.session.module, "tcp_v4_connect"); err != nil {
return nil, trace.Wrap(err)
if err := c.objs.MonitoredCgroups.Delete(&cgroupID); err != nil {
return trace.Wrap(err)
}
if err = AttachKprobe(c.session.module, "tcp_v6_connect"); err != nil {
return nil, trace.Wrap(err)
}
c.event4Buf, err = NewRingBuffer(c.session.module, network4EventsBuffer)
if err != nil {
return nil, trace.Wrap(err)
}
c.event6Buf, err = NewRingBuffer(c.session.module, network6EventsBuffer)
if err != nil {
return nil, trace.Wrap(err)
}
c.lost, err = NewCounter(c.session.module, "lost", lostNetworkEvents)
if err != nil {
return nil, trace.Wrap(err)
}
return c, nil
return nil
}
// close will stop reading events off the ring buffer and unload the BPF
// program. The ring buffer is closed as part of the module being closed.
func (c *conn) close() {
c.lost.Close()
c.event4Buf.Close()
c.event6Buf.Close()
c.session.module.Close()
// c.lost.Close()
c.mtx.Lock()
defer c.mtx.Unlock()
if c.closed {
return
}
c.closed = true
for _, link := range c.toClose {
if err := link.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close link", "error", err)
}
}
if err := c.objs.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close network objects", "error", err)
}
if err := c.lostCounter.Close(); err != nil {
logger.WarnContext(context.Background(), "failed to close network lost counter", "error", err)
}
logger.DebugContext(context.Background(), "Closed network BPF module")
}
// v4Events contains raw events off the perf buffer.
func (c *conn) v4Events() <-chan []byte {
return c.event4Buf.EventCh
return c.event4Chan
}
// v6Events contains raw events off the perf buffer.
func (c *conn) v6Events() <-chan []byte {
return c.event6Buf.EventCh
return c.event6Chan
}
+198
View File
@@ -0,0 +1,198 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64 && bpf
package bpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"structs"
"github.com/cilium/ebpf"
)
type networkIpv4DataT struct {
_ structs.HostLayout
Cgroup uint64
Ip uint64
Pid uint32
Saddr uint32
Daddr uint32
Dport uint16
Command [16]uint8
_ [2]byte
}
type networkIpv6DataT struct {
_ structs.HostLayout
Cgroup uint64
Ip uint64
Pid uint32
Saddr struct {
_ structs.HostLayout
In6U struct {
_ structs.HostLayout
U6Addr8 [16]uint8
}
}
Daddr struct {
_ structs.HostLayout
In6U struct {
_ structs.HostLayout
U6Addr8 [16]uint8
}
}
Dport uint16
Command [16]uint8
_ [2]byte
}
// loadNetwork returns the embedded CollectionSpec for network.
func loadNetwork() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_NetworkBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load network: %w", err)
}
return spec, err
}
// loadNetworkObjects loads network and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *networkObjects
// *networkPrograms
// *networkMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadNetworkObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadNetwork()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// networkSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkSpecs struct {
networkProgramSpecs
networkMapSpecs
networkVariableSpecs
}
// networkProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkProgramSpecs struct {
KprobeTcpV4Connect *ebpf.ProgramSpec `ebpf:"kprobe__tcp_v4_connect"`
KprobeTcpV6Connect *ebpf.ProgramSpec `ebpf:"kprobe__tcp_v6_connect"`
KretprobeTcpV4Connect *ebpf.ProgramSpec `ebpf:"kretprobe__tcp_v4_connect"`
KretprobeTcpV6Connect *ebpf.ProgramSpec `ebpf:"kretprobe__tcp_v6_connect"`
}
// networkMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkMapSpecs struct {
Currsock *ebpf.MapSpec `ebpf:"currsock"`
Ipv4Events *ebpf.MapSpec `ebpf:"ipv4_events"`
Ipv6Events *ebpf.MapSpec `ebpf:"ipv6_events"`
LostCounter *ebpf.MapSpec `ebpf:"lost_counter"`
LostDoorbell *ebpf.MapSpec `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.MapSpec `ebpf:"monitored_cgroups"`
}
// networkVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkVariableSpecs struct {
UnusedIpv4DataT *ebpf.VariableSpec `ebpf:"unused_ipv4_data_t"`
UnusedIpv6DataT *ebpf.VariableSpec `ebpf:"unused_ipv6_data_t"`
}
// networkObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkObjects struct {
networkPrograms
networkMaps
networkVariables
}
func (o *networkObjects) Close() error {
return _NetworkClose(
&o.networkPrograms,
&o.networkMaps,
)
}
// networkMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkMaps struct {
Currsock *ebpf.Map `ebpf:"currsock"`
Ipv4Events *ebpf.Map `ebpf:"ipv4_events"`
Ipv6Events *ebpf.Map `ebpf:"ipv6_events"`
LostCounter *ebpf.Map `ebpf:"lost_counter"`
LostDoorbell *ebpf.Map `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.Map `ebpf:"monitored_cgroups"`
}
func (m *networkMaps) Close() error {
return _NetworkClose(
m.Currsock,
m.Ipv4Events,
m.Ipv6Events,
m.LostCounter,
m.LostDoorbell,
m.MonitoredCgroups,
)
}
// networkVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkVariables struct {
UnusedIpv4DataT *ebpf.Variable `ebpf:"unused_ipv4_data_t"`
UnusedIpv6DataT *ebpf.Variable `ebpf:"unused_ipv6_data_t"`
}
// networkPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkPrograms struct {
KprobeTcpV4Connect *ebpf.Program `ebpf:"kprobe__tcp_v4_connect"`
KprobeTcpV6Connect *ebpf.Program `ebpf:"kprobe__tcp_v6_connect"`
KretprobeTcpV4Connect *ebpf.Program `ebpf:"kretprobe__tcp_v4_connect"`
KretprobeTcpV6Connect *ebpf.Program `ebpf:"kretprobe__tcp_v6_connect"`
}
func (p *networkPrograms) Close() error {
return _NetworkClose(
p.KprobeTcpV4Connect,
p.KprobeTcpV6Connect,
p.KretprobeTcpV4Connect,
p.KretprobeTcpV6Connect,
)
}
func _NetworkClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed network_arm64_bpfel.o
var _NetworkBytes []byte
Binary file not shown.
+198
View File
@@ -0,0 +1,198 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build (386 || amd64) && bpf
package bpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"structs"
"github.com/cilium/ebpf"
)
type networkIpv4DataT struct {
_ structs.HostLayout
Cgroup uint64
Ip uint64
Pid uint32
Saddr uint32
Daddr uint32
Dport uint16
Command [16]uint8
_ [2]byte
}
type networkIpv6DataT struct {
_ structs.HostLayout
Cgroup uint64
Ip uint64
Pid uint32
Saddr struct {
_ structs.HostLayout
In6U struct {
_ structs.HostLayout
U6Addr8 [16]uint8
}
}
Daddr struct {
_ structs.HostLayout
In6U struct {
_ structs.HostLayout
U6Addr8 [16]uint8
}
}
Dport uint16
Command [16]uint8
_ [2]byte
}
// loadNetwork returns the embedded CollectionSpec for network.
func loadNetwork() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_NetworkBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load network: %w", err)
}
return spec, err
}
// loadNetworkObjects loads network and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *networkObjects
// *networkPrograms
// *networkMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadNetworkObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadNetwork()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// networkSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkSpecs struct {
networkProgramSpecs
networkMapSpecs
networkVariableSpecs
}
// networkProgramSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkProgramSpecs struct {
KprobeTcpV4Connect *ebpf.ProgramSpec `ebpf:"kprobe__tcp_v4_connect"`
KprobeTcpV6Connect *ebpf.ProgramSpec `ebpf:"kprobe__tcp_v6_connect"`
KretprobeTcpV4Connect *ebpf.ProgramSpec `ebpf:"kretprobe__tcp_v4_connect"`
KretprobeTcpV6Connect *ebpf.ProgramSpec `ebpf:"kretprobe__tcp_v6_connect"`
}
// networkMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkMapSpecs struct {
Currsock *ebpf.MapSpec `ebpf:"currsock"`
Ipv4Events *ebpf.MapSpec `ebpf:"ipv4_events"`
Ipv6Events *ebpf.MapSpec `ebpf:"ipv6_events"`
LostCounter *ebpf.MapSpec `ebpf:"lost_counter"`
LostDoorbell *ebpf.MapSpec `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.MapSpec `ebpf:"monitored_cgroups"`
}
// networkVariableSpecs contains global variables before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type networkVariableSpecs struct {
UnusedIpv4DataT *ebpf.VariableSpec `ebpf:"unused_ipv4_data_t"`
UnusedIpv6DataT *ebpf.VariableSpec `ebpf:"unused_ipv6_data_t"`
}
// networkObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkObjects struct {
networkPrograms
networkMaps
networkVariables
}
func (o *networkObjects) Close() error {
return _NetworkClose(
&o.networkPrograms,
&o.networkMaps,
)
}
// networkMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkMaps struct {
Currsock *ebpf.Map `ebpf:"currsock"`
Ipv4Events *ebpf.Map `ebpf:"ipv4_events"`
Ipv6Events *ebpf.Map `ebpf:"ipv6_events"`
LostCounter *ebpf.Map `ebpf:"lost_counter"`
LostDoorbell *ebpf.Map `ebpf:"lost_doorbell"`
MonitoredCgroups *ebpf.Map `ebpf:"monitored_cgroups"`
}
func (m *networkMaps) Close() error {
return _NetworkClose(
m.Currsock,
m.Ipv4Events,
m.Ipv6Events,
m.LostCounter,
m.LostDoorbell,
m.MonitoredCgroups,
)
}
// networkVariables contains all global variables after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkVariables struct {
UnusedIpv4DataT *ebpf.Variable `ebpf:"unused_ipv4_data_t"`
UnusedIpv6DataT *ebpf.Variable `ebpf:"unused_ipv6_data_t"`
}
// networkPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadNetworkObjects or ebpf.CollectionSpec.LoadAndAssign.
type networkPrograms struct {
KprobeTcpV4Connect *ebpf.Program `ebpf:"kprobe__tcp_v4_connect"`
KprobeTcpV6Connect *ebpf.Program `ebpf:"kprobe__tcp_v6_connect"`
KretprobeTcpV4Connect *ebpf.Program `ebpf:"kretprobe__tcp_v4_connect"`
KretprobeTcpV6Connect *ebpf.Program `ebpf:"kretprobe__tcp_v6_connect"`
}
func (p *networkPrograms) Close() error {
return _NetworkClose(
p.KprobeTcpV4Connect,
p.KprobeTcpV6Connect,
p.KretprobeTcpV4Connect,
p.KretprobeTcpV6Connect,
)
}
func _NetworkClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed network_x86_bpfel.o
var _NetworkBytes []byte
Binary file not shown.
+9 -10
View File
@@ -31,6 +31,7 @@ import (
"bytes"
"context"
"encoding/binary"
"errors"
"os"
"path/filepath"
"regexp"
@@ -142,12 +143,10 @@ func (s *Service) Remove(sessionID string) error {
// Move all PIDs to the root controller. This has to be done before a cgroup
// can be removed.
err = writePids(filepath.Join(s.MountPath, cgroupProcs), pids)
if err != nil {
if err = writePids(filepath.Join(s.MountPath, cgroupProcs), pids); err != nil {
return trace.Wrap(err)
}
// The rmdir syscall is used to remove a cgroup.
err = unix.Rmdir(filepath.Join(s.teleportRoot, sessionID))
if err != nil {
return trace.Wrap(err)
@@ -209,7 +208,9 @@ func writePids(path string, pids []string) error {
for _, pid := range pids {
_, err := f.WriteString(pid + "\n")
if err != nil {
// ignore no such process that can be returned if the process has already
// exited.
if err != nil && !errors.Is(err, unix.ESRCH) {
return trace.Wrap(err)
}
}
@@ -217,7 +218,7 @@ func writePids(path string, pids []string) error {
return trace.Wrap(f.Sync())
}
// cleanupHierarchy removes any cgroups for any exisiting sessions.
// cleanupHierarchy removes any cgroups for any existing sessions.
func (s *Service) cleanupHierarchy() error {
var sessions []string
@@ -377,15 +378,13 @@ func (s *Service) ID(sessionID string) (uint64, error) {
return fh.CgroupID, nil
}
var (
// pattern matches cgroup process files.
pattern = regexp.MustCompile(`cgroup\.procs$`)
)
// pattern matches cgroup process files.
var pattern = regexp.MustCompile(`cgroup\.procs$`)
const (
// fileMode is the mode files and directories are created in within the
// cgroup filesystem.
fileMode = 0555
fileMode = 0o555
// teleportRoot is the prefix of the root cgroup that holds all other
// Teleport cgroups.
+2 -4
View File
@@ -542,13 +542,11 @@ func ReadableDatabaseProtocol(p string) string {
}
const (
// PerfBufferPageCount is the size of the perf ring buffer in number of pages.
// Must be power of 2.
PerfBufferPageCount = 8
// PerfBufferPageCount is the size of the perf ring buffer.
PerfBufferPageCount = 64
// OpenPerfBufferPageCount is the page count for the perf buffer. Open
// events generate many events so this buffer needs to be extra large.
// Must be power of 2.
OpenPerfBufferPageCount = 128
// CgroupPath is where the cgroupv2 hierarchy will be mounted.
+20 -6
View File
@@ -18,7 +18,11 @@
package servicecfg
import "github.com/gravitational/teleport/lib/defaults"
import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/defaults"
)
// BPFConfig holds configuration for the BPF service.
type BPFConfig struct {
@@ -43,17 +47,27 @@ type BPFConfig struct {
// CheckAndSetDefaults checks BPF configuration.
func (c *BPFConfig) CheckAndSetDefaults() error {
var perfBufferPageCount = defaults.PerfBufferPageCount
var openPerfBufferPageCount = defaults.OpenPerfBufferPageCount
perfBufferPageCount := defaults.PerfBufferPageCount
openPerfBufferPageCount := defaults.OpenPerfBufferPageCount
if c.CommandBufferSize == nil {
// Set defaults for buffer sizes if they are unset or zero.
// A zero value was accepted before but is undesirable now as it
// will result in blocking event channels, so we set it to a sane
// default to maintain backwards compatibility.
if c.CommandBufferSize == nil || *c.CommandBufferSize == 0 {
c.CommandBufferSize = &perfBufferPageCount
} else if *c.CommandBufferSize < 0 {
return trace.BadParameter("CommandBufferSize must not be negative")
}
if c.DiskBufferSize == nil {
if c.DiskBufferSize == nil || *c.DiskBufferSize == 0 {
c.DiskBufferSize = &openPerfBufferPageCount
} else if *c.DiskBufferSize < 0 {
return trace.BadParameter("DiskBufferSize must not be negative")
}
if c.NetworkBufferSize == nil {
if c.NetworkBufferSize == nil || *c.NetworkBufferSize == 0 {
c.NetworkBufferSize = &perfBufferPageCount
} else if *c.NetworkBufferSize < 0 {
return trace.BadParameter("NetworkBufferSize must not be negative")
}
if c.CgroupPath == "" {
c.CgroupPath = defaults.CgroupPath