diff options
Diffstat (limited to 'src/core/bpf')
-rw-r--r-- | src/core/bpf/meson.build | 111 | ||||
-rw-r--r-- | src/core/bpf/restrict_fs/meson.build | 24 | ||||
-rw-r--r-- | src/core/bpf/restrict_fs/restrict-fs-skel.h | 14 | ||||
-rw-r--r-- | src/core/bpf/restrict_fs/restrict-fs.bpf.c | 82 | ||||
-rw-r--r-- | src/core/bpf/restrict_ifaces/meson.build | 24 | ||||
-rw-r--r-- | src/core/bpf/restrict_ifaces/restrict-ifaces-skel.h | 14 | ||||
-rw-r--r-- | src/core/bpf/restrict_ifaces/restrict-ifaces.bpf.c | 52 | ||||
-rw-r--r-- | src/core/bpf/socket_bind/meson.build | 24 | ||||
-rw-r--r-- | src/core/bpf/socket_bind/socket-bind-api.bpf.h | 51 | ||||
-rw-r--r-- | src/core/bpf/socket_bind/socket-bind-skel.h | 14 | ||||
-rw-r--r-- | src/core/bpf/socket_bind/socket-bind.bpf.c | 111 |
11 files changed, 521 insertions, 0 deletions
diff --git a/src/core/bpf/meson.build b/src/core/bpf/meson.build new file mode 100644 index 0000000..f654016 --- /dev/null +++ b/src/core/bpf/meson.build @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +if conf.get('BPF_FRAMEWORK') != 1 + subdir_done() +endif + +bpf_clang_flags = [ + '-std=gnu11', + '-Wno-compare-distinct-pointer-types', + '-O2', + '-target', + 'bpf', + '-g', + '-c', +] + +bpf_gcc_flags = [ + '-std=gnu11', + '-O2', + '-mkernel=5.2', + '-mcpu=v3', + '-mco-re', + '-gbtf', + '-c', +] + +# Generate defines that are appropriate to tell the compiler what architecture +# we're compiling for. By default we just map meson's cpu_family to __<cpu_family>__. +# This dictionary contains the exceptions where this doesn't work. +# +# C.f. https://mesonbuild.com/Reference-tables.html#cpu-families +# and src/basic/missing_syscall_def.h. +cpu_arch_defines = { + 'ppc' : ['-D__powerpc__'], + 'ppc64' : ['-D__powerpc64__', '-D_CALL_ELF=2'], + 'riscv32' : ['-D__riscv', '-D__riscv_xlen=32'], + 'riscv64' : ['-D__riscv', '-D__riscv_xlen=64'], + 'x86' : ['-D__i386__'], + + # For arm, assume hardware fp is available. + 'arm' : ['-D__arm__', '-D__ARM_PCS_VFP'], +} + +bpf_arch_flags = cpu_arch_defines.get(host_machine.cpu_family(), + ['-D__@0@__'.format(host_machine.cpu_family())]) +if bpf_compiler == 'gcc' + bpf_arch_flags += ['-m' + host_machine.endian() + '-endian'] +endif + +libbpf_include_dir = libbpf.get_variable(pkgconfig : 'includedir') + +bpf_o_unstripped_cmd = [] +if bpf_compiler == 'clang' + bpf_o_unstripped_cmd += [ + clang, + bpf_clang_flags, + bpf_arch_flags, + ] +elif bpf_compiler == 'gcc' + bpf_o_unstripped_cmd += [ + bpf_gcc, + bpf_gcc_flags, + bpf_arch_flags, + ] +endif + +bpf_o_unstripped_cmd += ['-I.'] + +if not meson.is_cross_build() and bpf_compiler == 'clang' + target_triplet_cmd = run_command('gcc', '-dumpmachine', check: false) + if target_triplet_cmd.returncode() == 0 + target_triplet = target_triplet_cmd.stdout().strip() + bpf_o_unstripped_cmd += [ + '-isystem', + '/usr/include/@0@'.format(target_triplet) + ] + endif +endif + +bpf_o_unstripped_cmd += [ + '-idirafter', + libbpf_include_dir, + '@INPUT@', + '-o', + '@OUTPUT@' +] + +if bpftool_strip + bpf_o_cmd = [ + bpftool, + 'gen', + 'object', + '@OUTPUT@', + '@INPUT@' + ] +elif bpf_compiler == 'clang' + bpf_o_cmd = [ + llvm_strip, + '-g', + '@INPUT@', + '-o', + '@OUTPUT@' + ] +endif + +skel_h_cmd = [ + bpftool, + 'gen', + 'skeleton', + '@INPUT@' +] diff --git a/src/core/bpf/restrict_fs/meson.build b/src/core/bpf/restrict_fs/meson.build new file mode 100644 index 0000000..69cde02 --- /dev/null +++ b/src/core/bpf/restrict_fs/meson.build @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +if conf.get('BPF_FRAMEWORK') != 1 + subdir_done() +endif + +restrict_fs_bpf_o_unstripped = custom_target( + 'restrict-fs.bpf.unstripped.o', + input : 'restrict-fs.bpf.c', + output : 'restrict-fs.bpf.unstripped.o', + command : bpf_o_unstripped_cmd) + +restrict_fs_bpf_o = custom_target( + 'restrict-fs.bpf.o', + input : restrict_fs_bpf_o_unstripped, + output : 'restrict-fs.bpf.o', + command : bpf_o_cmd) + +restrict_fs_skel_h = custom_target( + 'restrict-fs.skel.h', + input : restrict_fs_bpf_o, + output : 'restrict-fs.skel.h', + command : skel_h_cmd, + capture : true) diff --git a/src/core/bpf/restrict_fs/restrict-fs-skel.h b/src/core/bpf/restrict_fs/restrict-fs-skel.h new file mode 100644 index 0000000..412cf62 --- /dev/null +++ b/src/core/bpf/restrict_fs/restrict-fs-skel.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +/* libbpf is used via dlopen(), so rename symbols */ +#define bpf_object__open_skeleton sym_bpf_object__open_skeleton +#define bpf_object__load_skeleton sym_bpf_object__load_skeleton +#define bpf_object__destroy_skeleton sym_bpf_object__destroy_skeleton + +#include "bpf/restrict_fs/restrict-fs.skel.h" diff --git a/src/core/bpf/restrict_fs/restrict-fs.bpf.c b/src/core/bpf/restrict_fs/restrict-fs.bpf.c new file mode 100644 index 0000000..eb5ed3e --- /dev/null +++ b/src/core/bpf/restrict_fs/restrict-fs.bpf.c @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +#include <linux/types.h> +#include <linux/bpf.h> +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> +#include <bpf/bpf_core_read.h> +#include <errno.h> +#include <stddef.h> +#include <stdint.h> + +struct super_block { + unsigned long int s_magic; +} __attribute__((preserve_access_index)); + +struct inode { + struct super_block *i_sb; +} __attribute__((preserve_access_index)); + +struct file { + struct inode *f_inode; +} __attribute__((preserve_access_index)); + +/* + * max_entries is set from user space with the bpf_map__set_max_entries helper. + * */ +struct { + __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); + __type(key, uint64_t); /* cgroup ID */ + __type(value, uint32_t); /* fs magic set */ +} cgroup_hash SEC(".maps"); + +SEC("lsm/file_open") +int BPF_PROG(restrict_filesystems, struct file *file, int ret) +{ + unsigned long raw_magic_number; + uint64_t cgroup_id; + uint32_t *value, *magic_map, magic_number, zero = 0, *is_allow; + + /* ret is the return value from the previous BPF program or 0 if it's + * the first hook */ + if (ret != 0) + return ret; + + BPF_CORE_READ_INTO(&raw_magic_number, file, f_inode, i_sb, s_magic); + /* super_block.s_magic is unsigned long, but magic_map keys are + * uint32_t. Using s_magic as-is would fail on big-endian systems, + * which have 64-bit unsigned long. So cast it. */ + magic_number = (uint32_t)raw_magic_number; + + cgroup_id = bpf_get_current_cgroup_id(); + + magic_map = bpf_map_lookup_elem(&cgroup_hash, &cgroup_id); + if (!magic_map) + return 0; + + is_allow = bpf_map_lookup_elem(magic_map, &zero); + if (!is_allow) + /* Malformed map, it doesn't include whether it's an allow list + * or a deny list. Allow. */ + return 0; + + if (*is_allow) { + /* Allow-list: Allow access only if magic_number present in inner map */ + if (!bpf_map_lookup_elem(magic_map, &magic_number)) + return -EPERM; + } else { + /* Deny-list: Allow access only if magic_number is not present in inner map */ + if (bpf_map_lookup_elem(magic_map, &magic_number)) + return -EPERM; + } + + return 0; +} + +static const char _license[] SEC("license") = "GPL"; diff --git a/src/core/bpf/restrict_ifaces/meson.build b/src/core/bpf/restrict_ifaces/meson.build new file mode 100644 index 0000000..5f36178 --- /dev/null +++ b/src/core/bpf/restrict_ifaces/meson.build @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +if conf.get('BPF_FRAMEWORK') != 1 + subdir_done() +endif + +restrict_ifaces_bpf_o_unstripped = custom_target( + 'restrict-ifaces.bpf.unstripped.o', + input : 'restrict-ifaces.bpf.c', + output : 'restrict-ifaces.bpf.unstripped.o', + command : bpf_o_unstripped_cmd) + +restrict_ifaces_bpf_o = custom_target( + 'restrict-ifaces.bpf.o', + input : restrict_ifaces_bpf_o_unstripped, + output : 'restrict-ifaces.bpf.o', + command : bpf_o_cmd) + +restrict_ifaces_skel_h = custom_target( + 'restrict-ifaces.skel.h', + input : restrict_ifaces_bpf_o, + output : 'restrict-ifaces.skel.h', + command : skel_h_cmd, + capture : true) diff --git a/src/core/bpf/restrict_ifaces/restrict-ifaces-skel.h b/src/core/bpf/restrict_ifaces/restrict-ifaces-skel.h new file mode 100644 index 0000000..f937490 --- /dev/null +++ b/src/core/bpf/restrict_ifaces/restrict-ifaces-skel.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +/* libbpf is used via dlopen(), so rename symbols */ +#define bpf_object__open_skeleton sym_bpf_object__open_skeleton +#define bpf_object__load_skeleton sym_bpf_object__load_skeleton +#define bpf_object__destroy_skeleton sym_bpf_object__destroy_skeleton + +#include "bpf/restrict_ifaces/restrict-ifaces.skel.h" diff --git a/src/core/bpf/restrict_ifaces/restrict-ifaces.bpf.c b/src/core/bpf/restrict_ifaces/restrict-ifaces.bpf.c new file mode 100644 index 0000000..32cde5c --- /dev/null +++ b/src/core/bpf/restrict_ifaces/restrict-ifaces.bpf.c @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* <linux/bpf.h> must precede <bpf/bpf_helpers.h> due to integer types + * in bpf helpers signatures. + */ +#include <linux/bpf.h> +#include <bpf/bpf_helpers.h> + +const volatile __u8 is_allow_list = 0; + +/* Map containing the network interfaces indexes. + * The interpretation of the map depends on the value of is_allow_list. + */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); + __type(value, __u8); +} sd_restrictif SEC(".maps"); + +#define DROP 0 +#define PASS 1 + +static __always_inline int restrict_network_interfaces_impl(const struct __sk_buff *sk) { + __u32 zero = 0, ifindex; + __u8 *lookup_result; + + ifindex = sk->ifindex; + lookup_result = bpf_map_lookup_elem(&sd_restrictif, &ifindex); + if (is_allow_list) { + /* allow-list: let the packet pass if iface in the list */ + if (lookup_result) + return PASS; + } else { + /* deny-list: let the packet pass if iface *not* in the list */ + if (!lookup_result) + return PASS; + } + + return DROP; +} + +SEC("cgroup_skb/egress") +int sd_restrictif_e(const struct __sk_buff *sk) { + return restrict_network_interfaces_impl(sk); +} + +SEC("cgroup_skb/ingress") +int sd_restrictif_i(const struct __sk_buff *sk) { + return restrict_network_interfaces_impl(sk); +} + +static const char _license[] SEC("license") = "LGPL-2.1-or-later"; diff --git a/src/core/bpf/socket_bind/meson.build b/src/core/bpf/socket_bind/meson.build new file mode 100644 index 0000000..05a2b9d --- /dev/null +++ b/src/core/bpf/socket_bind/meson.build @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +if conf.get('BPF_FRAMEWORK') != 1 + subdir_done() +endif + +socket_bind_bpf_o_unstripped = custom_target( + 'socket-bind.bpf.unstripped.o', + input : 'socket-bind.bpf.c', + output : 'socket-bind.bpf.unstripped.o', + command : bpf_o_unstripped_cmd) + +socket_bind_bpf_o = custom_target( + 'socket-bind.bpf.o', + input : socket_bind_bpf_o_unstripped, + output : 'socket-bind.bpf.o', + command : bpf_o_cmd) + +socket_bind_skel_h = custom_target( + 'socket-bind.skel.h', + input : socket_bind_bpf_o, + output : 'socket-bind.skel.h', + command : skel_h_cmd, + capture : true) diff --git a/src/core/bpf/socket_bind/socket-bind-api.bpf.h b/src/core/bpf/socket_bind/socket-bind-api.bpf.h new file mode 100644 index 0000000..277b9bb --- /dev/null +++ b/src/core/bpf/socket_bind/socket-bind-api.bpf.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +#include <linux/types.h> + +/* + * Bind rule is matched with socket fields accessible to cgroup/bind{4,6} hook + * through bpf_sock_addr struct. + * 'address_family' is expected to be one of AF_UNSPEC, AF_INET or AF_INET6. + * Matching by family is bypassed for rules with AF_UNSPEC set, which makes the + * rest of a rule applicable for both IPv4 and IPv6 addresses. + * If matching by family is either successful or bypassed, a rule and a socket + * are matched by ip protocol. + * If 'protocol' is 0, matching is bypassed. + * 'nr_ports' and 'port_min' fields specify a set of ports to match a user port + * with. + * If 'nr_ports' is 0, matching by port is bypassed, making that rule applicable + * for all possible ports, e.g. [1, 65535] range. Thus a rule with + * 'address_family', 'protocol' and 'nr_ports' equal to AF_UNSPEC, 0 and 0 + * correspondingly forms 'allow any' or 'deny any' cases. + * For positive 'nr_ports', a user_port lying in a range from 'port_min' to' + * 'port_min' + 'nr_ports' exclusively is considered to be a match. 'nr_ports' + * equalling to 1 forms a rule for a single port. + * Ports are in host order. + * + * Examples: + * AF_UNSPEC, 1, 0, 7777: match IPv4 and IPv6 addresses with 7777 user port; + * + * AF_INET, 1023, 0, 1: match IPv4 addresses with user port in [1, 1023] + * range inclusively; + * + * AF_INET6, 0, 0, 0: match IPv6 addresses; + * + * AF_UNSPEC, 0, 0, 0: match IPv4 and IPv6 addresses; + * + * AF_INET6, IPPROTO_TCP, 0, 0: match IPv6/TCP addresses. + */ + +struct socket_bind_rule { + __u32 address_family; + __u32 protocol; + __u16 nr_ports; + __u16 port_min; +}; + +#define SOCKET_BIND_MAX_RULES 128 diff --git a/src/core/bpf/socket_bind/socket-bind-skel.h b/src/core/bpf/socket_bind/socket-bind-skel.h new file mode 100644 index 0000000..e0d1626 --- /dev/null +++ b/src/core/bpf/socket_bind/socket-bind-skel.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +/* libbpf is used via dlopen(), so rename symbols */ +#define bpf_object__open_skeleton sym_bpf_object__open_skeleton +#define bpf_object__load_skeleton sym_bpf_object__load_skeleton +#define bpf_object__destroy_skeleton sym_bpf_object__destroy_skeleton + +#include "bpf/socket_bind/socket-bind.skel.h" diff --git a/src/core/bpf/socket_bind/socket-bind.bpf.c b/src/core/bpf/socket_bind/socket-bind.bpf.c new file mode 100644 index 0000000..b7972a8 --- /dev/null +++ b/src/core/bpf/socket_bind/socket-bind.bpf.c @@ -0,0 +1,111 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +#include "socket-bind-api.bpf.h" +/* <linux/types.h> must precede <bpf/bpf_helpers.h> due to + * <bpf/bpf_helpers.h> does not depend from type header by design. + */ +#include <linux/types.h> +#include <bpf/bpf_endian.h> +#include <bpf/bpf_helpers.h> +#include <linux/bpf.h> +#include <netinet/in.h> +#include <stdbool.h> + +/* + * max_entries is set from user space with bpf_map__set_max_entries helper. + */ +struct socket_bind_map_t { + __uint(type, BPF_MAP_TYPE_ARRAY); + __type(key, __u32); + __type(value, struct socket_bind_rule); +}; + +enum socket_bind_action { + SOCKET_BIND_DENY = 0, + SOCKET_BIND_ALLOW = 1, +}; + +struct socket_bind_map_t sd_bind_allow SEC(".maps"); +struct socket_bind_map_t sd_bind_deny SEC(".maps"); + +static __always_inline bool match_af( + __u8 address_family, const struct socket_bind_rule *r) { + return r->address_family == AF_UNSPEC || address_family == r->address_family; +} + +static __always_inline bool match_protocol( + __u32 protocol, const struct socket_bind_rule *r) { + return r->protocol == 0 || r->protocol == protocol; +} + +static __always_inline bool match_user_port( + __u16 port, const struct socket_bind_rule *r) { + return r->nr_ports == 0 || + (port >= r->port_min && port < r->port_min + (__u32) r->nr_ports); +} + +static __always_inline bool match( + __u8 address_family, + __u32 protocol, + __u16 port, + const struct socket_bind_rule *r) { + return match_af(address_family, r) && + match_protocol(protocol, r) && + match_user_port(port, r); +} + +static __always_inline bool match_rules( + struct bpf_sock_addr *ctx, + struct socket_bind_map_t *rules) { + volatile __u32 user_port = ctx->user_port; + __u16 port = (__u16)bpf_ntohs(user_port); + + for (__u32 i = 0; i < SOCKET_BIND_MAX_RULES; ++i) { + const __u32 key = i; + const struct socket_bind_rule *rule = bpf_map_lookup_elem(rules, &key); + + /* Lookup returns NULL if iterator is advanced past the last + * element put in the map. */ + if (!rule) + break; + + if (match(ctx->user_family, ctx->protocol, port, rule)) + return true; + } + + return false; +} + +static __always_inline int bind_socket(struct bpf_sock_addr *ctx) { + if (match_rules(ctx, &sd_bind_allow)) + return SOCKET_BIND_ALLOW; + + if (match_rules(ctx, &sd_bind_deny)) + return SOCKET_BIND_DENY; + + return SOCKET_BIND_ALLOW; +} + +SEC("cgroup/bind4") +int sd_bind4(struct bpf_sock_addr *ctx) { + if (ctx->user_family != AF_INET || ctx->family != AF_INET) + return SOCKET_BIND_ALLOW; + + return bind_socket(ctx); +} + +SEC("cgroup/bind6") +int sd_bind6(struct bpf_sock_addr *ctx) { + if (ctx->user_family != AF_INET6 || ctx->family != AF_INET6) + return SOCKET_BIND_ALLOW; + + return bind_socket(ctx); +} + +char _license[] SEC("license") = "GPL"; |