commit c93868ae18da29b96e28d4a6293253d762afc857 Author: Palo palo Date: Sun Jun 9 18:07:50 2024 +0200 First commit - implemented everything diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7753c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.output +netcat_ebpf_demo +tags diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0ec96d5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "includes/liblog"] + path = includes/liblog + url = https://github.com/rxi/log.c.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..90f2efe --- /dev/null +++ b/Makefile @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +OUTPUT := .output +CLANG ?= clang +LLVM_STRIP ?= llvm-strip +SHELL := /bin/bash +LIBLOG_OBJ := $(abspath $(OUTPUT)/liblog.o) +LIBLOG_SRC := $(abspath ./includes/liblog/src/log.c) +LIBLOG_HDR := $(abspath ./includes/liblog/src/) +INC := $(abspath ./includes/) +BPFTOOL := sudo bpftool +ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/') +INCLUDES := -I$(OUTPUT) -I$(LIBLOG_HDR) -I$(INC) +CFLAGS := -g -Wall -DLOG_USE_COLOR +ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) + +APPS = netcat_ebpf_demo +SOURCES := $(filter-out src/$(APPS).c,$(wildcard src/*.c)) + +ALL_LDFLAGS += -lrt -ldl -lpthread -lm -lbpf -lelf -lz + +# 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 - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') + +ifeq ($(V),1) + Q = + msg = +else + Q = @ + msg = @printf ' %-8s %s%s\n' \ + "$(1)" \ + "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ + "$(if $(3), $(3))"; + MAKEFLAGS += --no-print-directory +endif + +define allow-override + $(if $(or $(findstring environment,$(origin $(1))),\ + $(findstring command line,$(origin $(1)))),,\ + $(eval $(1) = $(2))) +endef + +$(call allow-override,CC,$(CROSS_COMPILE)cc) +$(call allow-override,LD,$(CROSS_COMPILE)ld) + +.PHONY: all +all: $(APPS) + +.PHONY: clean +clean: + $(call msg,CLEAN) + $(Q)rm -rf $(OUTPUT) $(APPS) + +clean-app: + $(call msg,CLEAN-APP) + $(Q)rm -rf $(APPS) + $(Q)rm -rf $(OUTPUT)/*.skel.h + $(Q)rm -rf $(OUTPUT)/*.o + +$(OUTPUT): + $(call msg,MKDIR,$@) + $(Q)mkdir -p $@ + +# Build liblog +$(LIBLOG_OBJ): + $(call msg,LIBLOG,$@) + $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(LIBLOG_SRC) -o $@ + +# Build BPF code +$(OUTPUT)/%.bpf.o: src/ebpf/%.bpf.c $(wildcard src/ebpf/%.h) $(VMLINUX) | $(OUTPUT) + $(call msg,BPF,$@) + $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o $@ + $(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info + +# Generate BPF skeletons +$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) + $(call msg,GEN-SKEL,$@) + $(Q)$(BPFTOOL) gen skeleton $< > $@ + +# Build user-space code +$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h + +$(OUTPUT)/%.o: src/%.c $(wildcard %.h) | $(OUTPUT) + $(call msg,CC,$@) + $(Q)$(CC) $(CFLAGS) -c $(filter %.c,$^) $(INCLUDES) -o $@ + +# Build application binary +$(APPS): %: $(OUTPUT)/%.o $(LIBLOG_OBJ) | $(OUTPUT) + $(call msg,BINARY,$@) + $(Q)$(CC) $(CFLAGS) $(SOURCES) $^ $(ALL_LDFLAGS) -o $@ + +# delete failed targets +.DELETE_ON_ERROR: + +# keep intermediate (.skel.h, .bpf.o, etc) targets +.SECONDARY: diff --git a/README.md b/README.md new file mode 100644 index 0000000..55b1ab6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Netcat eBPF Demo diff --git a/includes/common.h b/includes/common.h new file mode 100644 index 0000000..c262350 --- /dev/null +++ b/includes/common.h @@ -0,0 +1,17 @@ +#ifndef _NETCAT_EBPF_DEMO_COMMON_H +#define _NETCAT_EBPF_DEMO_COMMON_H + +// Constants +#define MAX_ENTRIES_SOCKMAP 1024 + +#define LOCALHOST 16777343 + +// Program indexes +enum { + PROG_SOCKOPS_INTERCEPT_NEW_CONNECTION = 0, + PROG_SK_SKB_VERDICT_REDIRECT, + + MAX_NUM_OF_PROGRAMS +}; + +#endif diff --git a/includes/ebpf_loader.h b/includes/ebpf_loader.h new file mode 100644 index 0000000..b09812d --- /dev/null +++ b/includes/ebpf_loader.h @@ -0,0 +1,9 @@ +#ifndef _NETCAT_EBPF_DEMO_EBPF_LOADER_H +#define _NETCAT_EBPF_DEMO_EBPF_LOADER_H + +// Include skeleton file +#include "../.output/netcat_ebpf_demo.skel.h" + +int ebpf_loader(int argc, const char **argv, struct netcat_ebpf_demo_bpf **obj); + +#endif diff --git a/includes/liblog b/includes/liblog new file mode 160000 index 0000000..f9ea349 --- /dev/null +++ b/includes/liblog @@ -0,0 +1 @@ +Subproject commit f9ea34994bd58ed342d2245cd4110bb5c6790153 diff --git a/src/ebpf/netcat_ebpf_demo.bpf.c b/src/ebpf/netcat_ebpf_demo.bpf.c new file mode 100644 index 0000000..726e3de --- /dev/null +++ b/src/ebpf/netcat_ebpf_demo.bpf.c @@ -0,0 +1,97 @@ +#include +#include +#include + +#include "../../includes/common.h" + +// Map to save sockets file descriptors +struct { + __uint(type, BPF_MAP_TYPE_SOCKMAP); + __uint(key_size, sizeof(int)); + __uint(value_size, sizeof(int)); + __uint(max_entries, MAX_ENTRIES_SOCKMAP); +} sockmap SEC(".maps"); + +/* SOCKOPS INTERCEPT CLIENT CONNECTION */ + +static int key = 2; + +/** + * Check the ports to intercept a new connection to port 3333, then add it + * to the sockmap + */ +SEC("sockops_intercept_new_connection") +int sockops_intercept_new_connection_main(struct bpf_sock_ops *ops) { + int err; + + switch (ops->op) { + case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: + // we are only interested in the packets going to port 3333 + if (ops->local_port != 3333) { + break; + } + + // add the new socket in the sockmap + // this way we can intercept its packets with the SK_SKB program + bpf_printk("New Socket with key %d from %d to %d", key, bpf_htonl(ops->remote_port), ops->local_port); + int err = bpf_sock_map_update(ops, &sockmap, &key, BPF_ANY); + if (err < 0) { + bpf_printk("Failed inserting new socket %d in sockmap"); + return 0; + } + key++; + break; + default: + return 0; + } + + return 0; +} + + +/* SK_SKB VERDICT REDIRECT */ + +/** + * On port 3333, redirect packets based on the content + */ +SEC("sk_skb_verdict_redirect") +int sk_skb_verdict_redirect_main(struct __sk_buff *skb) { + int err = 0; + + // we are only interested in the packets going to port 3333 + if (skb->local_port != 3333) { + return SK_PASS; + } + + // read the first letter of the packet + unsigned char data[1] = { 0 }; + bpf_skb_pull_data(skb, skb->len); + bpf_probe_read_kernel(data, 1, (void *) skb->data); + + // redirect on a different socket based on the first letter + int redirect_index; + switch (data[0]) { + case 'a': + redirect_index = 0; + break; + case 'b': + redirect_index = 1; + break; + default: + return SK_PASS; + } + + err = bpf_sk_redirect_map(skb, &sockmap, redirect_index, 0); + if (err < 0) { + bpf_printk("Failed redirecting to socket %d", redirect_index); + return SK_PASS; + } + + bpf_printk("Redirecting socket to socket %d", redirect_index); + bpf_printk(""); + + return SK_PASS; +} + + +char LICENSE[] SEC("license") = "GPL"; diff --git a/src/ebpf_loader.c b/src/ebpf_loader.c new file mode 100644 index 0000000..fa5fa9a --- /dev/null +++ b/src/ebpf_loader.c @@ -0,0 +1,105 @@ +#include +#include +#include + +#include "../includes/liblog/src/log.h" + +#include "../includes/common.h" +#include "../includes/ebpf_loader.h" + +struct program_description { + char name[256]; + enum bpf_prog_type type; + int map_prog_idx; + struct bpf_program *prog; +}; + +// Description of all the programs +static struct program_description progs[] = { + {"sockops_intercept_new_connection", BPF_PROG_TYPE_SOCK_OPS, PROG_SOCKOPS_INTERCEPT_NEW_CONNECTION, NULL}, + {"sk_skb_verdict_redirect", BPF_PROG_TYPE_SK_SKB, PROG_SK_SKB_VERDICT_REDIRECT, NULL}, +}; + +/** + * When pressing CTRL-C, close the program + */ +void sigint_handler(int sig_no) { + log_debug("Closing program..."); + exit(0); +} + +/** + * Load the eBPF programs specified in the ``progs`` array + */ +int ebpf_loader(int argc, const char **argv, struct netcat_ebpf_demo_bpf **obj) { + struct bpf_object_skeleton *skel = NULL; + int err; + + // open BPF application + *obj = netcat_ebpf_demo_bpf__open(); + if (!(*obj)) { + log_fatal("Error while opening BPF skeleton"); + return -1; + } + + skel = (*obj)->skeleton; + struct bpf_prog_skeleton *skeleton_programs = skel->progs; + + // set program types + for (int i = 0; i < skel->prog_cnt; i++) { + bpf_program__set_type(*(skeleton_programs[i].prog), progs[i].type); + } + + // load and verify BPF programs + if (netcat_ebpf_demo_bpf__load(*obj)) { + log_fatal("Error while loading BPF skeleton"); + return -1; + } + + // define routine to close the program gracefully + struct sigaction action; + memset(&action, 0, sizeof(action)); + action.sa_handler = &sigint_handler; + + if (sigaction(SIGINT, &action, NULL) == -1) { + log_error("sigation failed"); + goto cleanup; + } + + if (sigaction(SIGTERM, &action, NULL) == -1) { + log_error("sigation failed"); + goto cleanup; + } + + // attach the SOCKOPS program to the root cgroup + int cg_fd = open("/sys/fs/cgroup/", __O_DIRECTORY, O_RDONLY); + if (cg_fd < 0) { + log_fatal("Failed to set reuseaddr: %s", strerror(errno)); + goto cleanup; + } + + err = bpf_prog_attach(bpf_program__fd(*(skeleton_programs[PROG_SOCKOPS_INTERCEPT_NEW_CONNECTION].prog)), cg_fd, BPF_CGROUP_SOCK_OPS, 0); + if (err < 0) { + log_fatal("Failed to attach sockops: %s", strerror(errno)); + goto cleanup; + } + + // attach the SK_SKB program to the sockmap + const int sockmap_fd = bpf_map__fd((*obj)->maps.sockmap); + + err = bpf_prog_attach(bpf_program__fd(*(skeleton_programs[PROG_SK_SKB_VERDICT_REDIRECT].prog)), sockmap_fd, BPF_SK_SKB_STREAM_VERDICT, 0); + if (err) { + log_fatal("Failed to attach BPF verdict program: %s", strerror(errno)); + goto cleanup; + } + + log_info("Successfully attached all programs!"); + log_info(""); + + return 0; + +cleanup: + netcat_ebpf_demo_bpf__destroy((*obj)); + log_info("Program stopped correctly"); + return -err; +} diff --git a/src/netcat_ebpf_demo.c b/src/netcat_ebpf_demo.c new file mode 100644 index 0000000..2fba178 --- /dev/null +++ b/src/netcat_ebpf_demo.c @@ -0,0 +1,102 @@ +#include +#include + +#include + +#include "../includes/liblog/src/log.h" + +#include "../includes/common.h" +#include "../includes/ebpf_loader.h" + +/* All functions, unless specified otherwise, return 0 on success and a negative number on error */ + + +/** + * Connect to the address and port specified and put the + * socket file descriptor in the ``netcat_socket`` variable + */ +int connectToNetcat(int address, int port, int* netcat_socket) { + int err = 0; + + *netcat_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + struct sockaddr_in sin; + memset(&sin, 0, sizeof(struct sockaddr_in)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = address; + sin.sin_port = htons(port); + + err = connect(*netcat_socket, (struct sockaddr *) &sin, sizeof(struct sockaddr_in)); + if (err < 0) { + log_fatal("Connect on address %d and port %d failed with %s", address, port, strerror(errno)); + return err; + } + + log_info("Connected on address %d and port %d", address, port); + + return 0; +} + +/** + * Insert the two sockets in the sockmap, respectively at position 0 and 1 + */ +int insertSocketsInSockmap(int socket_A, int socket_B, int sockmap_fd) { + int err = 0; + + int zero = 0; + int one = 1; + + err = bpf_map_update_elem(sockmap_fd, &zero, &socket_A, BPF_ANY); + if (err < 0) { + log_fatal("Adding socket A to sockmap failed with %s", strerror(errno)); + return err; + } + + err = bpf_map_update_elem(sockmap_fd, &one, &socket_B, BPF_ANY); + if (err < 0) { + log_fatal("Adding socket B to sockmap failed with %s", strerror(errno)); + return err; + } + + return 0; +} + +/** + * Load all the eBPF programs, connect on ports 4444 and 5555, insert + * the sockets in the sockmap then wait forever + */ +int main(int argc, const char** argv) { + int err; + + // load all the eBPF files + struct netcat_ebpf_demo_bpf *obj; + err = ebpf_loader(argc, argv, &obj); + if (err < 0) { + log_fatal("Loading of eBPF programs failed with %s", strerror(errno)); + return -err; + } + + // create the two sockets, on port 4444 and 5555 + int socket_A, socket_B; + err = connectToNetcat(LOCALHOST, 4444, &socket_A); + if (err < 0) { + return -err; + } + + err = connectToNetcat(LOCALHOST, 5555, &socket_B); + if (err < 0) { + return -err; + } + + // insert the sockets in the sockmap, to make them visible to eBPF + int sockmap_fd = bpf_map__fd(obj->maps.sockmap); + err = insertSocketsInSockmap(socket_A, socket_B, sockmap_fd); + if (err < 0) { + return -err; + } + + // wait forever + while (1) {} + + return 0; +}