From 58a33faf8081e3b6126b49817a0b5cee54e8770b Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Fri, 13 Nov 2020 17:40:17 -0800 Subject: [PATCH 01/16] bpf: add socket-bind BPF program code sources Introduce BPF program compiled from BPF source code in restricted C - socket-bind. It addresses feature request [0]. The goal is to allow systemd services to bind(2) only to a predefined set of ports. This prevents assigning socket address with unallowed port to a socket and creating servers listening on that port. This compliments firewalling feature presenting in systemd: whereas cgroup/{egress|ingress} hooks act on packets, this doesn't protect from untrusted service or payload hijacking an important port. While ports in 0-1023 range are restricted to root only, 1024-65535 range is not protected by any mean. Performance is another aspect of socket_bind feature since per-packet cost can be eliminated for some port-based filtering policies. The feature is implemented with cgroup/bind{4|6} hooks [1]. In contrast to the present systemd approach using raw bpf instructions, this program is compiled from sources. Stretch goal is to make bpf ecosystem in systemd more friendly for developer and to clear path for more BPF programs. [0] https://github.com/systemd/systemd/pull/13496#issuecomment-570573085 [1] https://www.spinics.net/lists/netdev/msg489054.html --- .../bpf/socket_bind/socket-bind-api.bpf.h | 47 ++++++++ src/core/bpf/socket_bind/socket-bind.bpf.c | 103 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/core/bpf/socket_bind/socket-bind-api.bpf.h create mode 100644 src/core/bpf/socket_bind/socket-bind.bpf.c 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 0000000000..2f08408747 --- /dev/null +++ b/src/core/bpf/socket_bind/socket-bind-api.bpf.h @@ -0,0 +1,47 @@ +/* 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 + +/* + * 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 ports. + * nr_ports and port_min fields specify a set of ports to match a user port + * with. + * If nr_ports is 0, maching by port is bypassed, making that rule applicable + * for all possible ports, e.g. [1, 65535] range. Thus a rule with + * address_family and nr_ports equal to AF_UNSPEC 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, 7777: match IPv4 and IPv6 addresses with 7777 user port; + * + * AF_INET, 1023, 1: match IPv4 addresses with user port in [1, 1023] + * range inclusively; + * + * AF_INET6, 0, 0: match IPv6 addresses; + * + * AF_UNSPEC, 0, 0: match IPv4 and IPv6 addresses. + */ + +struct socket_bind_rule { + __u32 address_family; + __u16 nr_ports; + __u16 port_min; +}; + +#define SOCKET_BIND_MAX_RULES 128 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 0000000000..474808d824 --- /dev/null +++ b/src/core/bpf/socket_bind/socket-bind.bpf.c @@ -0,0 +1,103 @@ +/* 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" +/* must precede due to + * does not depend from type header by design. + */ +#include +#include +#include +#include +#include +#include + +/* + * max_entries is set from user space with bpf_map__resize 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_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, + __u16 port, + const struct socket_bind_rule *r) { + return match_af(address_family, 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, 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"; From cf4f9a57f20f1b21d59574e1f0cb6504506f1728 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Fri, 13 Nov 2020 17:02:50 -0800 Subject: [PATCH 02/16] bpf: add build script for bpf programs Add a build script to compile bpf source code. A program in restricted C is compiled into an object file. Object file is converted to BPF skeleton [0] header file. If build with custom meson build rule, the target header will reside in build/ directory (not in source tree), e.g the path for socket_bind: `build/src/core/bpf/socket_bind/socket-bind.skel.h` Script runs the phases: * clang to generate *.o from restricted C * llvm-strip to remove useless DWARF info * bpf skeleton generation with bpftool These phases are logged to stderr for debug purposes. To include BTF debug information, -g option is passed to clang. [0] https://lwn.net/Articles/806911/ --- tools/build-bpf-skel.py | 123 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100755 tools/build-bpf-skel.py diff --git a/tools/build-bpf-skel.py b/tools/build-bpf-skel.py new file mode 100755 index 0000000000..5506fdd875 --- /dev/null +++ b/tools/build-bpf-skel.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1+ + +import argparse +import logging +import pathlib +import subprocess +import sys + +def clang_arch_flag(arch): + return '-D__{}__'.format(arch) + + +def target_triplet(): + gcc_exec = 'gcc' + + try: + return subprocess.check_output([gcc_exec, '-dumpmachine'], + universal_newlines=True).strip() + except subprocess.CalledProcessError as e: + logging.error('Failed to get target triplet: {}'.format(e)) + except FileNotFoundError: + logging.error('gcc not installed') + return None + + +def clang_compile(clang_exec, clang_flags, src_c, out_file, target_arch, + target_triplet): + clang_args = [clang_exec, *clang_flags, target_arch, '-I.'] + + if target_triplet: + clang_args += [ + '-isystem', + '/usr/include/{}'.format(target_triplet)] + + clang_args += [ + '-idirafter', + '/usr/local/include', + '-idirafter', + '/usr/include'] + + clang_args += [src_c, '-o', out_file] + + logging.debug('{}'.format(' '.join(clang_args))) + subprocess.check_call(clang_args) + + +def llvm_strip(llvm_strip_exec, in_file): + llvm_strip_args = [llvm_strip_exec, '-g', in_file] + + logging.debug('Stripping useless DWARF info:') + logging.debug('{}'.format(' '.join(llvm_strip_args))) + + subprocess.check_call(llvm_strip_args) + + +def gen_bpf_skeleton(bpftool_exec, in_file, out_fd): + bpftool_args = [bpftool_exec, 'g', 's', in_file] + + logging.debug('Generating BPF skeleton:') + logging.debug('{}'.format(' '.join(bpftool_args))) + subprocess.check_call(bpftool_args, stdout=out_fd) + + +def bpf_build(args): + clang_flags = [ + '-Wno-compare-distinct-pointer-types', + '-O2', + '-target', + 'bpf', + '-g', + '-c', + ] + + clang_out_path = pathlib.Path(args.bpf_src_c).with_suffix('.o') + with clang_out_path.open(mode='w') as clang_out, \ + open(args.bpf_skel_h, mode='w') as bpf_skel_h: + clang_compile(clang_exec=args.clang_exec, + clang_flags=clang_flags, + src_c=args.bpf_src_c, + out_file=clang_out.name, + target_arch=clang_arch_flag(args.arch), + target_triplet=target_triplet()) + + llvm_strip(llvm_strip_exec=args.llvm_strip_exec, in_file=clang_out.name) + + gen_bpf_skeleton(bpftool_exec=args.bpftool_exec, + in_file=clang_out.name, + out_fd=bpf_skel_h) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + parser.add_argument( + 'bpf_src_c', + help='Path to *.c source of BPF program in systemd source tree \ + relative to the work directory') + + parser.add_argument( + 'bpf_skel_h', + help='Path to C header file') + + parser.add_argument( + '--clang_exec', + help='Path to clang exec') + + parser.add_argument( + '--llvm_strip_exec', + help='Path to llvm-strip exec') + + parser.add_argument( + '--bpftool_exec', + help='Path to bpftool exec') + + parser.add_argument( + '--arch', + help='Target CPU architecture', + default='x86_64') + + args = parser.parse_args(); + + logging.basicConfig(stream=sys.stderr, level=logging.WARNING) + bpf_build(args) From 7d861e12637892f1fcf7bbd69e4aec5a096f4bcb Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Fri, 13 Nov 2020 17:08:15 -0800 Subject: [PATCH 03/16] meson, bpf: add HAVE_LIBBPF, BPF_FRAMEWORK options * Add `bpf-framework` feature gate with 'auto', 'true' and 'false' choices * Add libbpf [0] dependency * Search for clang llvm-strip and bpftool binaries in compile time to generate bpf skeleton. For libbpf [0], make 0.2.0 [1] the minimum required version. If libbpf is satisfied, set HAVE_LIBBPF config option to 1. If `bpf-framework` feature gate is set to 'auto', means that whether bpf feature is enabled or now is defined by the presence of all of libbpf, clang, llvm and bpftool in build environment. With 'auto' all dependencies are optional. If the gate is set to `true`, make all of the libbpf, clang and llvm dependencies mandatory. If it's set to `false`, set `BPF_FRAMEWORK` to false and make libbpf dependency optional. libbpf dependency is dynamic followed by the common pattern in systemd. meson, bpf: add build rule for socket_bind program --- meson.build | 23 +++++++++++++++++++++++ meson_options.txt | 3 +++ 2 files changed, 26 insertions(+) diff --git a/meson.build b/meson.build index 81db2d27ab..93d3c26a22 100644 --- a/meson.build +++ b/meson.build @@ -937,6 +937,25 @@ if not libcap.found() libcap = cc.find_library('cap') endif +want_bpf_framework = get_option('bpf-framework') +bpf_framework_required = want_bpf_framework == 'true' + +libbpf = dependency('libbpf', required : bpf_framework_required, version : '>= 0.2') +conf.set10('HAVE_LIBBPF', libbpf.found()) + +if want_bpf_framework == 'false' + conf.set10('BPF_FRAMEWORK', 0) +else + clang = find_program('clang', required : bpf_framework_required) + llvm_strip = find_program('llvm-strip', required : bpf_framework_required) + bpftool = find_program('bpftool', required : bpf_framework_required) + bpf_arches = ['x86_64'] + deps_found = libbpf.found() and clang.found() and llvm_strip.found() and bpftool.found() + # Can build BPF program from source code in restricted C + conf.set10('BPF_FRAMEWORK', + bpf_arches.contains(host_machine.cpu_family()) and deps_found) +endif + libmount = dependency('mount', version : fuzzer_build ? '>= 0' : '>= 2.30') @@ -1604,6 +1623,7 @@ conf.set10('ENABLE_EFI', have) ############################################################ +build_bpf_skel_py = find_program('tools/build-bpf-skel.py') generate_gperfs = find_program('tools/generate-gperfs.py') make_autosuspend_rules_py = find_program('tools/make-autosuspend-rules.py') make_directive_index_py = find_program('tools/make-directive-index.py') @@ -1696,6 +1716,7 @@ install_libsystemd_static = static_library( libxz, libzstd, liblz4, + libbpf, libcap, libblkid, libmount, @@ -3766,6 +3787,7 @@ foreach tuple : [ ['elfutils'], ['gcrypt'], ['gnutls'], + ['libbpf'], ['libcryptsetup'], ['libcurl'], ['libfdisk'], @@ -3792,6 +3814,7 @@ foreach tuple : [ # components ['backlight'], ['binfmt'], + ['bpf-framework', conf.get('BPF_FRAMEWORK') == 1], ['coredump'], ['environment.d'], ['efi'], diff --git a/meson_options.txt b/meson_options.txt index 9441b88dec..ad7174cf69 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -403,3 +403,6 @@ option('kernel-install', type: 'boolean', value: 'true', description : 'install kernel-install and associated files') option('analyze', type: 'boolean', value: 'true', description : 'install systemd-analyze') + +option('bpf-framework', type : 'combo', choices : ['auto', 'true', 'false'], + description: 'build BPF programs from source code in restricted C') From 09fc220c52fad8fdc8648ab5b8162b822b6565b6 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Mon, 19 Apr 2021 23:30:08 -0700 Subject: [PATCH 04/16] meson, bpf: add build rule for socket-bind program --- src/core/bpf/socket_bind/meson.build | 14 ++++++++++++++ src/core/meson.build | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 src/core/bpf/socket_bind/meson.build diff --git a/src/core/bpf/socket_bind/meson.build b/src/core/bpf/socket_bind/meson.build new file mode 100644 index 0000000000..0ea8e5796d --- /dev/null +++ b/src/core/bpf/socket_bind/meson.build @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1+ + +if conf.get('BPF_FRAMEWORK') == 1 + socket_bind_skel_h = custom_target( + 'socket-bind.skel.h', + input : 'socket-bind.bpf.c', + output : 'socket-bind.skel.h', + command : [build_bpf_skel_py, + '--clang_exec', clang.path(), + '--llvm_strip_exec', llvm_strip.path(), + '--bpftool_exec', bpftool.path(), + '--arch', host_machine.cpu_family(), + '@INPUT@', '@OUTPUT@']) +endif diff --git a/src/core/meson.build b/src/core/meson.build index a1294f3a72..f8027333a9 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -123,6 +123,11 @@ libcore_sources = ''' unit.h '''.split() +subdir('bpf/socket_bind') +if conf.get('BPF_FRAMEWORK') == 1 + libcore_sources += [socket_bind_skel_h] +endif + load_fragment_gperf_gperf = custom_target( 'load-fragment-gperf.gperf', input : 'load-fragment-gperf.gperf.m4', From 43b3f0fb00541cce01daadee8e67192464eec76e Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Thu, 11 Feb 2021 19:38:36 -0800 Subject: [PATCH 05/16] shared, bpf: add bpf link helpers add can_link_bpf_program and bpf_link_free helpers. --- src/shared/bpf-link.c | 22 ++++++++++++++++++++++ src/shared/bpf-link.h | 12 ++++++++++++ src/shared/meson.build | 8 ++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/shared/bpf-link.c create mode 100644 src/shared/bpf-link.h diff --git a/src/shared/bpf-link.c b/src/shared/bpf-link.c new file mode 100644 index 0000000000..94d1a8c7ac --- /dev/null +++ b/src/shared/bpf-link.c @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include "bpf-link.h" + +bool can_link_bpf_program(struct bpf_program *prog) { + _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; + + assert(prog); + + /* Pass invalid cgroup fd intentionally. */ + link = bpf_program__attach_cgroup(prog, /*cgroup_fd=*/-1); + + /* EBADF indicates that bpf_link is supported by kernel. */ + return libbpf_get_error(link) == -EBADF; +} + +struct bpf_link *bpf_link_free(struct bpf_link *link) { + /* bpf_link__destroy handles link == NULL case */ + (void) bpf_link__destroy(link); + + return NULL; +} diff --git a/src/shared/bpf-link.h b/src/shared/bpf-link.h new file mode 100644 index 0000000000..7d529ad2cd --- /dev/null +++ b/src/shared/bpf-link.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#pragma once + +#include + +#include "macro.h" + +bool can_link_bpf_program(struct bpf_program *prog); + +struct bpf_link *bpf_link_free(struct bpf_link *p); +DEFINE_TRIVIAL_CLEANUP_FUNC(struct bpf_link *, bpf_link_free); diff --git a/src/shared/meson.build b/src/shared/meson.build index a2e4958fdc..4b590be1e0 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -319,6 +319,13 @@ if conf.get('HAVE_LIBIPTC') == 1 shared_sources += files('firewall-util-iptables.c') endif +if conf.get('HAVE_LIBBPF') == 1 + shared_sources += files(''' + bpf-link.c + bpf-link.h + '''.split()) +endif + if conf.get('HAVE_KMOD') == 1 shared_sources += files('module-util.c') endif @@ -378,6 +385,7 @@ libshared_name = 'systemd-shared-@0@'.format(meson.project_version()) libshared_deps = [threads, libacl, libblkid, + libbpf, libcap, libcrypt, libgcrypt, From b18e9fc167ff21be44a4aded536f80316aa84beb Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Wed, 10 Mar 2021 11:44:07 -0800 Subject: [PATCH 06/16] cgroup: add socket-bind to cgroup context --- src/core/cgroup.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/core/cgroup.h | 13 +++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/core/cgroup.c b/src/core/cgroup.c index 8b5df7610c..87c2b0dd9f 100644 --- a/src/core/cgroup.c +++ b/src/core/cgroup.c @@ -200,6 +200,18 @@ void cgroup_context_remove_bpf_foreign_program(CGroupContext *c, CGroupBPFForeig free(p); } +void cgroup_context_remove_socket_bind(CGroupSocketBindItem **head) { + CGroupSocketBindItem *h; + + assert(head); + + while (*head) { + h = *head; + LIST_REMOVE(socket_bind_items, *head, h); + free(h); + } +} + void cgroup_context_done(CGroupContext *c) { assert(c); @@ -221,6 +233,9 @@ void cgroup_context_done(CGroupContext *c) { while (c->device_allow) cgroup_context_free_device_allow(c, c->device_allow); + cgroup_context_remove_socket_bind(&c->socket_bind_allow); + cgroup_context_remove_socket_bind(&c->socket_bind_deny); + c->ip_address_allow = ip_address_access_free_all(c->ip_address_allow); c->ip_address_deny = ip_address_access_free_all(c->ip_address_deny); @@ -376,6 +391,7 @@ void cgroup_context_dump(Unit *u, FILE* f, const char *prefix) { CGroupBPFForeignProgram *p; CGroupDeviceAllow *a; CGroupContext *c; + CGroupSocketBindItem *bi; IPAddressAccessItem *iaai; char **path; char q[FORMAT_TIMESPAN_MAX]; @@ -562,6 +578,34 @@ void cgroup_context_dump(Unit *u, FILE* f, const char *prefix) { LIST_FOREACH(programs, p, c->bpf_foreign_programs) fprintf(f, "%sBPFProgram: %s:%s", prefix, bpf_cgroup_attach_type_to_string(p->attach_type), p->bpffs_path); + + if (c->socket_bind_allow) { + fprintf(f, "%sSocketBindAllow:", prefix); + LIST_FOREACH(socket_bind_items, bi, c->socket_bind_allow) + cgroup_context_dump_socket_bind_item(bi, f); + fputc('\n', f); + } + + if (c->socket_bind_deny) { + fprintf(f, "%sSocketBindDeny:", prefix); + LIST_FOREACH(socket_bind_items, bi, c->socket_bind_deny) + cgroup_context_dump_socket_bind_item(bi, f); + fputc('\n', f); + } +} + +void cgroup_context_dump_socket_bind_item(const CGroupSocketBindItem *item, FILE *f) { + const char *family = item->address_family == AF_INET ? "IPv4:" : + item->address_family == AF_INET6 ? "IPv6:" : ""; + + if (item->nr_ports == 0) + fprintf(f, " %sany", family); + else if (item->nr_ports == 1) + fprintf(f, " %s%" PRIu16, family, item->port_min); + else { + uint16_t port_max = item->port_min + item->nr_ports - 1; + fprintf(f, " %s%" PRIu16 "-%" PRIu16, family, item->port_min, port_max); + } } int cgroup_add_device_allow(CGroupContext *c, const char *dev, const char *mode) { diff --git a/src/core/cgroup.h b/src/core/cgroup.h index be3060eba7..1ad5dd3838 100644 --- a/src/core/cgroup.h +++ b/src/core/cgroup.h @@ -32,6 +32,7 @@ typedef struct CGroupIODeviceLatency CGroupIODeviceLatency; typedef struct CGroupBlockIODeviceWeight CGroupBlockIODeviceWeight; typedef struct CGroupBlockIODeviceBandwidth CGroupBlockIODeviceBandwidth; typedef struct CGroupBPFForeignProgram CGroupBPFForeignProgram; +typedef struct CGroupSocketBindItem CGroupSocketBindItem; typedef enum CGroupDevicePolicy { /* When devices listed, will allow those, plus built-in ones, if none are listed will allow @@ -101,6 +102,13 @@ struct CGroupBPFForeignProgram { char *bpffs_path; }; +struct CGroupSocketBindItem { + LIST_FIELDS(CGroupSocketBindItem, socket_bind_items); + int address_family; + uint16_t nr_ports; + uint16_t port_min; +}; + struct CGroupContext { bool cpu_accounting; bool io_accounting; @@ -165,6 +173,9 @@ struct CGroupContext { CGroupDevicePolicy device_policy; LIST_HEAD(CGroupDeviceAllow, device_allow); + LIST_HEAD(CGroupSocketBindItem, socket_bind_allow); + LIST_HEAD(CGroupSocketBindItem, socket_bind_deny); + /* Common */ TasksMax tasks_max; @@ -203,6 +214,7 @@ usec_t cgroup_cpu_adjust_period(usec_t period, usec_t quota, usec_t resolution, void cgroup_context_init(CGroupContext *c); void cgroup_context_done(CGroupContext *c); void cgroup_context_dump(Unit *u, FILE* f, const char *prefix); +void cgroup_context_dump_socket_bind_item(const CGroupSocketBindItem *item, FILE *f); void cgroup_context_free_device_allow(CGroupContext *c, CGroupDeviceAllow *a); void cgroup_context_free_io_device_weight(CGroupContext *c, CGroupIODeviceWeight *w); @@ -211,6 +223,7 @@ void cgroup_context_free_io_device_latency(CGroupContext *c, CGroupIODeviceLaten void cgroup_context_free_blockio_device_weight(CGroupContext *c, CGroupBlockIODeviceWeight *w); void cgroup_context_free_blockio_device_bandwidth(CGroupContext *c, CGroupBlockIODeviceBandwidth *b); void cgroup_context_remove_bpf_foreign_program(CGroupContext *c, CGroupBPFForeignProgram *p); +void cgroup_context_remove_socket_bind(CGroupSocketBindItem **head); int cgroup_add_device_allow(CGroupContext *c, const char *dev, const char *mode); int cgroup_add_bpf_foreign_program(CGroupContext *c, uint32_t attach_type, const char *path); From 91ce91c76c0d224556fdb002fa6c8b40bff604e0 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Sun, 25 Apr 2021 19:10:40 -0700 Subject: [PATCH 07/16] core, bpf: add socket-bind feature to unit Add supported and install unit interface for socket-bind feature. supported verifies that - unified cgroup hierarchy (cgroup v2) is used - BPF_FRAMEWORK (libbpf + clang + llvm + bpftool) was available in compile time - kernel supports BPF_PROG_TYPE_CGROUP_SOCK_ADDR - bpf programs can be loaded into kernel - bpf link can be used install: - load bpf_object from bpf skeleton - resize rules map to fit socket_bind_allow and socket_bind deny rules from cgroup context - populate cgroup-bpf maps with rules - get bpf programs from bpf skeleton - attach programs to unit cgroup using bpf link - save bpf link in the unit --- src/core/meson.build | 3 + src/core/socket-bind.c | 190 +++++++++++++++++++++++++++++++++++++++++ src/core/socket-bind.h | 8 ++ src/core/unit.c | 9 ++ src/core/unit.h | 8 ++ 5 files changed, 218 insertions(+) create mode 100644 src/core/socket-bind.c create mode 100644 src/core/socket-bind.h diff --git a/src/core/meson.build b/src/core/meson.build index f8027333a9..c5c9e8cbe8 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -105,6 +105,8 @@ libcore_sources = ''' slice.h smack-setup.c smack-setup.h + socket-bind.c + socket-bind.h socket.c socket.h swap.c @@ -157,6 +159,7 @@ libcore = static_library( include_directories : includes, dependencies : [versiondep, threads, + libbpf, librt, libseccomp, libpam, diff --git a/src/core/socket-bind.c b/src/core/socket-bind.c new file mode 100644 index 0000000000..f03bed5888 --- /dev/null +++ b/src/core/socket-bind.c @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#if BPF_FRAMEWORK +#include +#endif + +#include "fd-util.h" +#include "socket-bind.h" + +#if BPF_FRAMEWORK +/* libbpf, clang, llvm and bpftool compile time dependencies are satisfied */ +#include "bpf-link.h" +#include "bpf/socket_bind/socket-bind.skel.h" +#include "bpf/socket_bind/socket-bind-api.bpf.h" + +static struct socket_bind_bpf *socket_bind_bpf_free(struct socket_bind_bpf *obj) { + /* socket_bind_bpf__destroy handles object == NULL case */ + (void) socket_bind_bpf__destroy(obj); + + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct socket_bind_bpf *, socket_bind_bpf_free); + +static int update_rules_map( + int map_fd, CGroupSocketBindItem *head) { + CGroupSocketBindItem *item; + uint32_t i = 0; + + assert(map_fd >= 0); + + LIST_FOREACH(socket_bind_items, item, head) { + const uint32_t key = i++; + struct socket_bind_rule val = { + .address_family = (uint32_t) item->address_family, + .nr_ports = item->nr_ports, + .port_min = item->port_min, + }; + + if (bpf_map_update_elem(map_fd, &key, &val, BPF_ANY) != 0) + return -errno; + } + + return 0; +} + +static int prepare_socket_bind_bpf( + Unit *u, CGroupSocketBindItem *allow, CGroupSocketBindItem *deny, struct socket_bind_bpf **ret_obj) { + _cleanup_(socket_bind_bpf_freep) struct socket_bind_bpf *obj = 0; + uint32_t allow_count = 0, deny_count = 0; + int allow_map_fd, deny_map_fd, r; + CGroupSocketBindItem *item; + + assert(ret_obj); + + LIST_FOREACH(socket_bind_items, item, allow) + allow_count++; + + LIST_FOREACH(socket_bind_items, item, deny) + deny_count++; + + if (allow_count > SOCKET_BIND_MAX_RULES) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(EINVAL), + "Maximum number of socket bind rules=%u is exceeded", SOCKET_BIND_MAX_RULES); + + if (deny_count > SOCKET_BIND_MAX_RULES) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(EINVAL), + "Maximum number of socket bind rules=%u is exceeded", SOCKET_BIND_MAX_RULES); + + obj = socket_bind_bpf__open(); + if (!obj) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOMEM), "Failed to open BPF object"); + + if (bpf_map__resize(obj->maps.sd_bind_allow, MAX(allow_count, 1u)) != 0) + return log_unit_error_errno(u, errno, + "Failed to resize BPF map '%s': %m", bpf_map__name(obj->maps.sd_bind_allow)); + + if (bpf_map__resize(obj->maps.sd_bind_deny, MAX(deny_count, 1u)) != 0) + return log_unit_error_errno(u, errno, + "Failed to resize BPF map '%s': %m", bpf_map__name(obj->maps.sd_bind_deny)); + + if (socket_bind_bpf__load(obj) != 0) + return log_unit_error_errno(u, errno, "Failed to load BPF object"); + + allow_map_fd = bpf_map__fd(obj->maps.sd_bind_allow); + assert(allow_map_fd >= 0); + + r = update_rules_map(allow_map_fd, allow); + if (r < 0) + return log_unit_error_errno( + u, r, "Failed to put socket bind allow rules into BPF map '%s'", + bpf_map__name(obj->maps.sd_bind_allow)); + + deny_map_fd = bpf_map__fd(obj->maps.sd_bind_deny); + assert(deny_map_fd >= 0); + + r = update_rules_map(deny_map_fd, deny); + if (r < 0) + return log_unit_error_errno( + u, r, "Failed to put socket bind deny rules into BPF map '%s'", + bpf_map__name(obj->maps.sd_bind_deny)); + + *ret_obj = TAKE_PTR(obj); + return 0; +} + +int socket_bind_supported(void) { + _cleanup_(socket_bind_bpf_freep) struct socket_bind_bpf *obj = NULL; + + int r = cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER); + if (r < 0) + return log_error_errno(r, "Can't determine whether the unified hierarchy is used: %m"); + + if (r == 0) { + log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Not running with unified cgroup hierarchy, BPF is not supported"); + return 0; + } + + if (!bpf_probe_prog_type(BPF_PROG_TYPE_CGROUP_SOCK_ADDR, /*ifindex=*/0)) { + log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "BPF program type cgroup_sock_addr is not supported"); + return 0; + } + + r = prepare_socket_bind_bpf(/*unit=*/NULL, /*allow_rules=*/NULL, /*deny_rules=*/NULL, &obj); + if (r < 0) { + log_debug_errno(r, "BPF based socket_bind is not supported: %m"); + return 0; + } + + return can_link_bpf_program(obj->progs.sd_bind4); +} + +int socket_bind_install(Unit *u) { + _cleanup_(bpf_link_freep) struct bpf_link *ipv4 = NULL, *ipv6 = NULL; + _cleanup_(socket_bind_bpf_freep) struct socket_bind_bpf *obj = NULL; + _cleanup_free_ char *cgroup_path = NULL; + _cleanup_close_ int cgroup_fd = -1; + CGroupContext *cc; + int r; + + cc = unit_get_cgroup_context(u); + if (!cc) + return 0; + + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, u->cgroup_path, NULL, &cgroup_path); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to get cgroup path: %m"); + + if (!cc->socket_bind_allow && !cc->socket_bind_deny) + return 0; + + r = prepare_socket_bind_bpf(u, cc->socket_bind_allow, cc->socket_bind_deny, &obj); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to load BPF object: %m"); + + cgroup_fd = open(cgroup_path, O_RDONLY | O_CLOEXEC, 0); + if (cgroup_fd < 0) + return log_unit_error_errno( + u, errno, "Failed to open cgroup=%s for reading", cgroup_path); + + ipv4 = bpf_program__attach_cgroup(obj->progs.sd_bind4, cgroup_fd); + r = libbpf_get_error(ipv4); + if (r != 0) + return log_unit_error_errno(u, r, "Failed to link '%s' cgroup-bpf program", + bpf_program__name(obj->progs.sd_bind4)); + + ipv6 = bpf_program__attach_cgroup(obj->progs.sd_bind6, cgroup_fd); + r = libbpf_get_error(ipv6); + if (r != 0) + return log_unit_error_errno(u, r, "Failed to link '%s' cgroup-bpf program", + bpf_program__name(obj->progs.sd_bind6)); + + u->ipv4_socket_bind_link = TAKE_PTR(ipv4); + u->ipv6_socket_bind_link = TAKE_PTR(ipv6); + + return 0; +} +#else /* ! BPF_FRAMEWORK */ +int socket_bind_supported(void) { + return 0; +} + +int socket_bind_install(Unit *u) { + log_unit_debug(u, "Failed to install socket bind: BPF framework is not supported"); + return 0; +} + +#endif diff --git a/src/core/socket-bind.h b/src/core/socket-bind.h new file mode 100644 index 0000000000..58b73579ac --- /dev/null +++ b/src/core/socket-bind.h @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "unit.h" + +int socket_bind_supported(void); + +int socket_bind_install(Unit *u); diff --git a/src/core/unit.c b/src/core/unit.c index 864bcd3d6e..e8c0631429 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -41,6 +41,7 @@ #include "rm-rf.h" #include "set.h" #include "signal-util.h" +#include "socket-bind.h" #include "sparse-endian.h" #include "special.h" #include "specifier.h" @@ -56,6 +57,9 @@ #include "unit.h" #include "user-util.h" #include "virt.h" +#if BPF_FRAMEWORK +#include "bpf-link.h" +#endif /* Thresholds for logging at INFO level about resource consumption */ #define MENTIONWORTHY_CPU_NSEC (1 * NSEC_PER_SEC) @@ -663,6 +667,11 @@ Unit* unit_free(Unit *u) { if (u->on_console) manager_unref_console(u->manager); +#if BPF_FRAMEWORK + bpf_link_free(u->ipv4_socket_bind_link); + bpf_link_free(u->ipv6_socket_bind_link); +#endif + unit_release_cgroup(u); if (!MANAGER_IS_RELOADING(u->manager)) diff --git a/src/core/unit.h b/src/core/unit.h index 88d1074aac..a124376b78 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -309,6 +309,14 @@ typedef struct Unit { * attached to unit cgroup by provided program fd and attach type. */ Hashmap *bpf_foreign_by_key; +#if BPF_FRAMEWORK + /* BPF links to BPF programs attached to cgroup/bind{4|6} hooks and + * responsible for allowing or denying a unit to bind(2) to a socket + * address. */ + struct bpf_link *ipv4_socket_bind_link; + struct bpf_link *ipv6_socket_bind_link; +#endif + uint64_t ip_accounting_extra[_CGROUP_IP_ACCOUNTING_METRIC_MAX]; /* Low-priority event source which is used to remove watched PIDs that have gone away, and subscribe to any new From a8e5eb1788df23f0c120c21bfc6652227727d54b Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Thu, 12 Nov 2020 14:05:15 -0800 Subject: [PATCH 08/16] core: add socket-bind cgroup mask harness Standard cgroup harness for bpf feature. --- src/basic/cgroup-util.c | 1 + src/basic/cgroup-util.h | 4 +++- src/core/cgroup.c | 29 +++++++++++++++++++++++++++++ src/test/test-cgroup-mask.c | 2 +- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/basic/cgroup-util.c b/src/basic/cgroup-util.c index 1ac1f6dff0..b790df6dbe 100644 --- a/src/basic/cgroup-util.c +++ b/src/basic/cgroup-util.c @@ -2164,6 +2164,7 @@ static const char *const cgroup_controller_table[_CGROUP_CONTROLLER_MAX] = { [CGROUP_CONTROLLER_BPF_FIREWALL] = "bpf-firewall", [CGROUP_CONTROLLER_BPF_DEVICES] = "bpf-devices", [CGROUP_CONTROLLER_BPF_FOREIGN] = "bpf-foreign", + [CGROUP_CONTROLLER_BPF_SOCKET_BIND] = "bpf-socket-bind", }; DEFINE_STRING_TABLE_LOOKUP(cgroup_controller, CGroupController); diff --git a/src/basic/cgroup-util.h b/src/basic/cgroup-util.h index 8894fd9b0a..ce2f4c6589 100644 --- a/src/basic/cgroup-util.h +++ b/src/basic/cgroup-util.h @@ -31,6 +31,7 @@ typedef enum CGroupController { CGROUP_CONTROLLER_BPF_FIREWALL, CGROUP_CONTROLLER_BPF_DEVICES, CGROUP_CONTROLLER_BPF_FOREIGN, + CGROUP_CONTROLLER_BPF_SOCKET_BIND, _CGROUP_CONTROLLER_MAX, _CGROUP_CONTROLLER_INVALID = -EINVAL, @@ -51,6 +52,7 @@ typedef enum CGroupMask { CGROUP_MASK_BPF_FIREWALL = CGROUP_CONTROLLER_TO_MASK(CGROUP_CONTROLLER_BPF_FIREWALL), CGROUP_MASK_BPF_DEVICES = CGROUP_CONTROLLER_TO_MASK(CGROUP_CONTROLLER_BPF_DEVICES), CGROUP_MASK_BPF_FOREIGN = CGROUP_CONTROLLER_TO_MASK(CGROUP_CONTROLLER_BPF_FOREIGN), + CGROUP_MASK_BPF_SOCKET_BIND = CGROUP_CONTROLLER_TO_MASK(CGROUP_CONTROLLER_BPF_SOCKET_BIND), /* All real cgroup v1 controllers */ CGROUP_MASK_V1 = CGROUP_MASK_CPU|CGROUP_MASK_CPUACCT|CGROUP_MASK_BLKIO|CGROUP_MASK_MEMORY|CGROUP_MASK_DEVICES|CGROUP_MASK_PIDS, @@ -59,7 +61,7 @@ typedef enum CGroupMask { CGROUP_MASK_V2 = CGROUP_MASK_CPU|CGROUP_MASK_CPUSET|CGROUP_MASK_IO|CGROUP_MASK_MEMORY|CGROUP_MASK_PIDS, /* All cgroup v2 BPF pseudo-controllers */ - CGROUP_MASK_BPF = CGROUP_MASK_BPF_FIREWALL|CGROUP_MASK_BPF_DEVICES|CGROUP_MASK_BPF_FOREIGN, + CGROUP_MASK_BPF = CGROUP_MASK_BPF_FIREWALL|CGROUP_MASK_BPF_DEVICES|CGROUP_MASK_BPF_FOREIGN|CGROUP_MASK_BPF_SOCKET_BIND, _CGROUP_MASK_ALL = CGROUP_CONTROLLER_TO_MASK(_CGROUP_CONTROLLER_MAX) - 1 } CGroupMask; diff --git a/src/core/cgroup.c b/src/core/cgroup.c index 87c2b0dd9f..d541c16f5f 100644 --- a/src/core/cgroup.c +++ b/src/core/cgroup.c @@ -25,6 +25,7 @@ #include "percent-util.h" #include "process-util.h" #include "procfs-util.h" +#include "socket-bind.h" #include "special.h" #include "stat-util.h" #include "stdio-util.h" @@ -1099,6 +1100,12 @@ static void cgroup_apply_firewall(Unit *u) { (void) bpf_firewall_install(u); } +static void cgroup_apply_socket_bind(Unit *u) { + assert(u); + + (void) socket_bind_install(u); +} + static int cgroup_apply_devices(Unit *u) { _cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL; const char *path; @@ -1527,6 +1534,9 @@ static void cgroup_context_apply( if (apply_mask & CGROUP_MASK_BPF_FOREIGN) cgroup_apply_bpf_foreign_program(u); + + if (apply_mask & CGROUP_MASK_BPF_SOCKET_BIND) + cgroup_apply_socket_bind(u); } static bool unit_get_needs_bpf_firewall(Unit *u) { @@ -1570,6 +1580,17 @@ static bool unit_get_needs_bpf_foreign_program(Unit *u) { return !LIST_IS_EMPTY(c->bpf_foreign_programs); } +static bool unit_get_needs_socket_bind(Unit *u) { + CGroupContext *c; + assert(u); + + c = unit_get_cgroup_context(u); + if (!c) + return false; + + return c->socket_bind_allow != NULL || c->socket_bind_deny != NULL; +} + static CGroupMask unit_get_cgroup_mask(Unit *u) { CGroupMask mask = 0; CGroupContext *c; @@ -1624,6 +1645,9 @@ static CGroupMask unit_get_bpf_mask(Unit *u) { if (unit_get_needs_bpf_foreign_program(u)) mask |= CGROUP_MASK_BPF_FOREIGN; + if (unit_get_needs_socket_bind(u)) + mask |= CGROUP_MASK_BPF_SOCKET_BIND; + return mask; } @@ -3107,6 +3131,11 @@ static int cg_bpf_mask_supported(CGroupMask *ret) { if (r > 0) mask |= CGROUP_MASK_BPF_FOREIGN; + /* BPF-based bind{4|6} hooks */ + r = socket_bind_supported(); + if (r > 0) + mask |= CGROUP_MASK_BPF_SOCKET_BIND; + *ret = mask; return 0; } diff --git a/src/test/test-cgroup-mask.c b/src/test/test-cgroup-mask.c index d721946f71..3edc2754e1 100644 --- a/src/test/test-cgroup-mask.c +++ b/src/test/test-cgroup-mask.c @@ -140,7 +140,7 @@ static void test_cg_mask_to_string_one(CGroupMask mask, const char *t) { static void test_cg_mask_to_string(void) { test_cg_mask_to_string_one(0, NULL); - test_cg_mask_to_string_one(_CGROUP_MASK_ALL, "cpu cpuacct cpuset io blkio memory devices pids bpf-firewall bpf-devices bpf-foreign"); + test_cg_mask_to_string_one(_CGROUP_MASK_ALL, "cpu cpuacct cpuset io blkio memory devices pids bpf-firewall bpf-devices bpf-foreign bpf-socket-bind"); test_cg_mask_to_string_one(CGROUP_MASK_CPU, "cpu"); test_cg_mask_to_string_one(CGROUP_MASK_CPUACCT, "cpuacct"); test_cg_mask_to_string_one(CGROUP_MASK_CPUSET, "cpuset"); From 8dd210ab666201a236890b17bc74032bf71d6f73 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Tue, 20 Apr 2021 00:09:51 -0700 Subject: [PATCH 09/16] core: add SocketBind{Allow|Deny} fragment parser --- src/core/load-fragment-gperf.gperf.m4 | 4 +- src/core/load-fragment.c | 68 +++++++++++++++++++++++++++ src/core/load-fragment.h | 1 + 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4 index 4bd1207e2c..c531380401 100644 --- a/src/core/load-fragment-gperf.gperf.m4 +++ b/src/core/load-fragment-gperf.gperf.m4 @@ -235,7 +235,9 @@ $1.ManagedOOMMemoryPressure, config_parse_managed_oom_mode, $1.ManagedOOMMemoryPressureLimit, config_parse_managed_oom_mem_pressure_limit, 0, offsetof($1, cgroup_context.moom_mem_pressure_limit) $1.ManagedOOMPreference, config_parse_managed_oom_preference, 0, offsetof($1, cgroup_context.moom_preference) $1.NetClass, config_parse_warn_compat, DISABLED_LEGACY, 0 -$1.BPFProgram, config_parse_bpf_foreign_program, 0, offsetof($1, cgroup_context)' +$1.BPFProgram, config_parse_bpf_foreign_program, 0, offsetof($1, cgroup_context) +$1.SocketBindAllow, config_parse_cgroup_socket_bind, 0, offsetof($1, cgroup_context.socket_bind_allow) +$1.SocketBindDeny, config_parse_cgroup_socket_bind, 0, offsetof($1, cgroup_context.socket_bind_deny)' )m4_dnl Unit.Description, config_parse_unit_string_printf, 0, offsetof(Unit, description) Unit.Documentation, config_parse_documentation, 0, offsetof(Unit, documentation) diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c index 9be495e1ef..4f506e51e8 100644 --- a/src/core/load-fragment.c +++ b/src/core/load-fragment.c @@ -55,6 +55,7 @@ #endif #include "securebits-util.h" #include "signal-util.h" +#include "socket-bind.h" #include "socket-netlink.h" #include "stat-util.h" #include "string-util.h" @@ -5657,6 +5658,73 @@ int config_parse_bpf_foreign_program( return 0; } +int config_parse_cgroup_socket_bind( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + _cleanup_free_ CGroupSocketBindItem *item = NULL; + const char *address_family = NULL, *user_port; + uint16_t nr_ports = 0, port_min = 0; + CGroupSocketBindItem **head = data; + _cleanup_free_ char *word = NULL; + int af = AF_UNSPEC, r; + + if (isempty(rvalue)) { + cgroup_context_remove_socket_bind(head); + return 0; + } + + r = extract_first_word(&rvalue, &word, ":", 0); + if (r == -ENOMEM) + return log_oom(); + + if (rvalue) + address_family = word; + + if (address_family) { + if (streq(address_family, "IPv4")) + af = AF_INET; + else if (streq(address_family, "IPv6")) + af = AF_INET6; + else + return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), + "Only IPv4 or IPv6 protocols are supported, ignoring"); + } + + user_port = rvalue ?: word; + if (!streq(user_port, "any")) { + uint16_t port_max; + + r = parse_ip_port_range(user_port, &port_min, &port_max); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_warning_errno(r, "Invalid port or port range, ignoring: %m"); + + nr_ports = 1 + port_max - port_min; + } + + item = new(CGroupSocketBindItem, 1); + if (!item) + return log_oom(); + *item = (CGroupSocketBindItem) { + .address_family = af, + .nr_ports = nr_ports, + .port_min = port_min, + }; + + LIST_PREPEND(socket_bind_items, *head, TAKE_PTR(item)); + + return 0; +} + static int merge_by_names(Unit **u, Set *names, const char *id) { char *k; int r; diff --git a/src/core/load-fragment.h b/src/core/load-fragment.h index e99c9a4055..d722041f96 100644 --- a/src/core/load-fragment.h +++ b/src/core/load-fragment.h @@ -141,6 +141,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_mount_images); CONFIG_PARSER_PROTOTYPE(config_parse_socket_timestamping); CONFIG_PARSER_PROTOTYPE(config_parse_extension_images); CONFIG_PARSER_PROTOTYPE(config_parse_bpf_foreign_program); +CONFIG_PARSER_PROTOTYPE(config_parse_cgroup_socket_bind); /* gperf prototypes */ const struct ConfigPerfItem* load_fragment_gperf_lookup(const char *key, GPERF_LEN_TYPE length); From 3d027d4d60d37de89eaebf2d106ec0c6a492db72 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Mon, 19 Apr 2021 13:27:07 -0700 Subject: [PATCH 10/16] shared, bpf: add bpf link serialization core: serialize socket_bind bpf links --- src/core/socket-bind.c | 50 ++++++++++++++++++++++++++++++++++++++- src/core/socket-bind.h | 7 ++++++ src/core/unit-serialize.c | 20 ++++++++++++++++ src/core/unit.c | 2 ++ src/core/unit.h | 1 + src/shared/bpf-link.c | 16 +++++++++++++ src/shared/bpf-link.h | 4 ++++ 7 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/core/socket-bind.c b/src/core/socket-bind.c index f03bed5888..ed6c36c11f 100644 --- a/src/core/socket-bind.c +++ b/src/core/socket-bind.c @@ -132,7 +132,25 @@ int socket_bind_supported(void) { return can_link_bpf_program(obj->progs.sd_bind4); } -int socket_bind_install(Unit *u) { +int socket_bind_add_initial_link_fd(Unit *u, int fd) { + int r; + + assert(u); + + if (!u->initial_socket_bind_link_fds) { + u->initial_socket_bind_link_fds = fdset_new(); + if (!u->initial_socket_bind_link_fds) + return log_oom(); + } + + r = fdset_put(u->initial_socket_bind_link_fds, fd); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to put socket-bind BPF link fd %d to initial fdset", fd); + + return 0; +} + +static int socket_bind_install_impl(Unit *u) { _cleanup_(bpf_link_freep) struct bpf_link *ipv4 = NULL, *ipv6 = NULL; _cleanup_(socket_bind_bpf_freep) struct socket_bind_bpf *obj = NULL; _cleanup_free_ char *cgroup_path = NULL; @@ -177,14 +195,44 @@ int socket_bind_install(Unit *u) { return 0; } + +int socket_bind_install(Unit *u) { + int r = socket_bind_install_impl(u); + if (r == -ENOMEM) + return r; + + fdset_close(u->initial_socket_bind_link_fds); + + return r; +} + +int serialize_socket_bind(Unit *u, FILE *f, FDSet *fds) { + int r; + + assert(u); + + r = serialize_bpf_link(f, fds, "ipv4-socket-bind-bpf-link", u->ipv4_socket_bind_link); + if (r < 0) + return r; + + return serialize_bpf_link(f, fds, "ipv6-socket-bind-bpf-link", u->ipv6_socket_bind_link); +} + #else /* ! BPF_FRAMEWORK */ int socket_bind_supported(void) { return 0; } +int socket_bind_add_initial_link_fd(Unit *u, int fd) { + return 0; +} + int socket_bind_install(Unit *u) { log_unit_debug(u, "Failed to install socket bind: BPF framework is not supported"); return 0; } +int serialize_socket_bind(Unit *u, FILE *f, FDSet *fds) { + return 0; +} #endif diff --git a/src/core/socket-bind.h b/src/core/socket-bind.h index 58b73579ac..2a6e71a9b9 100644 --- a/src/core/socket-bind.h +++ b/src/core/socket-bind.h @@ -1,8 +1,15 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #pragma once +#include "fdset.h" #include "unit.h" int socket_bind_supported(void); +/* Add BPF link fd created before daemon-reload or daemon-reexec. + * FDs will be closed at the end of socket_bind_install. */ +int socket_bind_add_initial_link_fd(Unit *u, int fd); + int socket_bind_install(Unit *u); + +int serialize_socket_bind(Unit *u, FILE *f, FDSet *fds); diff --git a/src/core/unit-serialize.c b/src/core/unit-serialize.c index 3f099248ce..509210ad5e 100644 --- a/src/core/unit-serialize.c +++ b/src/core/unit-serialize.c @@ -7,6 +7,7 @@ #include "format-util.h" #include "parse-util.h" #include "serialize.h" +#include "socket-bind.h" #include "string-table.h" #include "unit-serialize.h" #include "user-util.h" @@ -151,6 +152,8 @@ int unit_serialize(Unit *u, FILE *f, FDSet *fds, bool serialize_jobs) { (void) serialize_cgroup_mask(f, "cgroup-enabled-mask", u->cgroup_enabled_mask); (void) serialize_cgroup_mask(f, "cgroup-invalidated-mask", u->cgroup_invalidated_mask); + (void) serialize_socket_bind(u, f, fds); + if (uid_is_valid(u->ref_uid)) (void) serialize_item_format(f, "ref-uid", UID_FMT, u->ref_uid); if (gid_is_valid(u->ref_gid)) @@ -362,6 +365,23 @@ int unit_deserialize(Unit *u, FILE *f, FDSet *fds) { else if (MATCH_DESERIALIZE_IMMEDIATE("cgroup-invalidated-mask", l, v, cg_mask_from_string, u->cgroup_invalidated_mask)) continue; + else if (STR_IN_SET(l, "ipv4-socket-bind-bpf-link-fd", "ipv6-socket-bind-bpf-link-fd")) { + int fd; + + if (safe_atoi(v, &fd) < 0 || fd < 0 || !fdset_contains(fds, fd)) + log_unit_debug(u, "Failed to parse %s value: %s, ignoring.", l, v); + else { + if (fdset_remove(fds, fd) < 0) { + log_unit_debug(u, "Failed to remove %s value=%d from fdset", l, fd); + + continue; + } + + (void) socket_bind_add_initial_link_fd(u, fd); + } + continue; + } + else if (streq(l, "ref-uid")) { uid_t uid; diff --git a/src/core/unit.c b/src/core/unit.c index e8c0631429..6ef90eea0e 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -667,6 +667,8 @@ Unit* unit_free(Unit *u) { if (u->on_console) manager_unref_console(u->manager); + + fdset_free(u->initial_socket_bind_link_fds); #if BPF_FRAMEWORK bpf_link_free(u->ipv4_socket_bind_link); bpf_link_free(u->ipv6_socket_bind_link); diff --git a/src/core/unit.h b/src/core/unit.h index a124376b78..5bc1237ce0 100644 --- a/src/core/unit.h +++ b/src/core/unit.h @@ -309,6 +309,7 @@ typedef struct Unit { * attached to unit cgroup by provided program fd and attach type. */ Hashmap *bpf_foreign_by_key; + FDSet *initial_socket_bind_link_fds; #if BPF_FRAMEWORK /* BPF links to BPF programs attached to cgroup/bind{4|6} hooks and * responsible for allowing or denying a unit to bind(2) to a socket diff --git a/src/shared/bpf-link.c b/src/shared/bpf-link.c index 94d1a8c7ac..f718cb7d2d 100644 --- a/src/shared/bpf-link.c +++ b/src/shared/bpf-link.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include "bpf-link.h" +#include "serialize.h" bool can_link_bpf_program(struct bpf_program *prog) { _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; @@ -14,6 +15,21 @@ bool can_link_bpf_program(struct bpf_program *prog) { return libbpf_get_error(link) == -EBADF; } +int serialize_bpf_link(FILE *f, FDSet *fds, const char *key, struct bpf_link *link) { + int fd; + + assert(key); + + if (!link) + return -ENOENT; + + if (libbpf_get_error(link) != 0) + return -EINVAL; + + fd = bpf_link__fd(link); + return serialize_fd(f, fds, key, fd); +} + struct bpf_link *bpf_link_free(struct bpf_link *link) { /* bpf_link__destroy handles link == NULL case */ (void) bpf_link__destroy(link); diff --git a/src/shared/bpf-link.h b/src/shared/bpf-link.h index 7d529ad2cd..095465b07c 100644 --- a/src/shared/bpf-link.h +++ b/src/shared/bpf-link.h @@ -3,10 +3,14 @@ #pragma once #include +#include +#include "fdset.h" #include "macro.h" bool can_link_bpf_program(struct bpf_program *prog); +int serialize_bpf_link(FILE *f, FDSet *fds, const char *key, struct bpf_link *link); + struct bpf_link *bpf_link_free(struct bpf_link *p); DEFINE_TRIVIAL_CLEANUP_FUNC(struct bpf_link *, bpf_link_free); From 28b76fc82aa16664353f417b39b6769842ea8502 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Mon, 16 Nov 2020 00:26:44 -0800 Subject: [PATCH 11/16] tests: add test program for SocketBind{Allow|Deny}= Verify that service exited correctly if valid ports are passed to SocketBind{Allow|Deny}= Use `ncat` program starting a listening service binding to a specified port, e.g. "timeout --preserve-status -sSIGTERM 1s /bin/nc -l -p ${port} -vv" --- src/test/meson.build | 9 +++ src/test/test-socket-bind.c | 151 ++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 src/test/test-socket-bind.c diff --git a/src/test/meson.build b/src/test/meson.build index 991c0f027d..2b8bdcc064 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -611,3 +611,12 @@ tests += [ [libudev], [threads]], ] + +tests += [ + [['src/test/test-socket-bind.c'], + [libcore, + libshared], + [libbpf], + core_includes, + 'BPF_FRAMEWORK'], +] diff --git a/src/test/test-socket-bind.c b/src/test/test-socket-bind.c new file mode 100644 index 0000000000..bfe5072bc3 --- /dev/null +++ b/src/test/test-socket-bind.c @@ -0,0 +1,151 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "load-fragment.h" +#include "manager.h" +#include "process-util.h" +#include "rlimit-util.h" +#include "rm-rf.h" +#include "service.h" +#include "socket-bind.h" +#include "strv.h" +#include "tests.h" +#include "unit.h" +#include "virt.h" + +static int find_netcat_executable(char **ret_path) { + char **candidates = STRV_MAKE("ncat", "nc", "netcat"), **c; + int r = 0; + + STRV_FOREACH(c, candidates) { + r = find_executable(*c, ret_path); + if (r == 0) + break; + } + + return r; +} + +static int test_socket_bind( + Manager *m, + const char *unit_name, + const char *netcat_path, + const char *port, + char **allow_rules, + char **deny_rules) { + _cleanup_free_ char *exec_start = NULL; + _cleanup_(unit_freep) Unit *u = NULL; + CGroupSocketBindItem *bi; + CGroupContext *cc = NULL; + char **rule; + int cld_code, r; + + assert_se(u = unit_new(m, sizeof(Service))); + assert_se(unit_add_name(u, unit_name) == 0); + assert_se(cc = unit_get_cgroup_context(u)); + + STRV_FOREACH(rule, allow_rules) { + r = config_parse_cgroup_socket_bind( + u->id, "filename", 1, "Service", 1, "SocketBindAllow", 0, + *rule, &cc->socket_bind_allow, u); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to parse SocketBindAllow: %m"); + } + + fprintf(stderr, "SocketBindAllow:"); + LIST_FOREACH(socket_bind_items, bi, cc->socket_bind_allow) + cgroup_context_dump_socket_bind_item(bi, stderr); + fputc('\n', stderr); + + STRV_FOREACH(rule, deny_rules) { + r = config_parse_cgroup_socket_bind( + u->id, "filename", 1, "Service", 1, "SocketBindDeny", 0, + *rule, &cc->socket_bind_deny, u); + if (r < 0) + return log_unit_error_errno(u, r, "Failed to parse SocketBindDeny: %m"); + } + + fprintf(stderr, "SocketBindDeny:"); + LIST_FOREACH(socket_bind_items, bi, cc->socket_bind_deny) + cgroup_context_dump_socket_bind_item(bi, stderr); + fputc('\n', stderr); + + exec_start = strjoin("-timeout --preserve-status -sSIGTERM 1s ", netcat_path, " -l ", port, " -vv"); + assert_se(exec_start != NULL); + + r = config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart", + SERVICE_EXEC_START, exec_start, SERVICE(u)->exec_command, u); + if (r < 0) + return log_error_errno(r, "Failed to parse ExecStart"); + + SERVICE(u)->type = SERVICE_ONESHOT; + u->load_state = UNIT_LOADED; + + r = unit_start(u); + if (r < 0) + return log_error_errno(r, "Unit start failed %m"); + + while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) { + r = sd_event_run(m->event, UINT64_MAX); + if (r < 0) + return log_error_errno(errno, "Event run failed %m"); + } + + cld_code = SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code; + if (cld_code != CLD_EXITED) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "ExecStart didn't exited, code='%s'", sigchld_code_to_string(cld_code)); + + if (SERVICE(u)->state != SERVICE_DEAD) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Service is not dead"); + + return 0; +} + +int main(int argc, char *argv[]) { + _cleanup_free_ char *unit_dir = NULL, *netcat_path = NULL; + _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + struct rlimit rl; + int r; + + test_setup_logging(LOG_DEBUG); + + if (detect_container() > 0) + return log_tests_skipped("test-bpf fails inside LXC and Docker containers: https://github.com/systemd/systemd/issues/9666"); + + if (getuid() != 0) + return log_tests_skipped("not running as root"); + + assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0); + rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE); + (void) setrlimit_closest(RLIMIT_MEMLOCK, &rl); + + if (!can_memlock()) + return log_tests_skipped("Can't use mlock(), skipping."); + + r = socket_bind_supported(); + if (r <= 0) + return log_tests_skipped("socket-bind is not supported, skipping."); + + if (find_netcat_executable(&netcat_path) != 0) + return log_tests_skipped("Can not find netcat executable, skipping."); + + r = enter_cgroup_subroot(NULL); + if (r == -ENOMEDIUM) + return log_tests_skipped("cgroupfs not available"); + + assert_se(get_testdata_dir("units", &unit_dir) >= 0); + assert_se(set_unit_path(unit_dir) >= 0); + assert_se(runtime_dir = setup_fake_runtime_dir()); + + assert_se(manager_new(UNIT_FILE_USER, MANAGER_TEST_RUN_BASIC, &m) >= 0); + assert_se(manager_startup(m, NULL, NULL) >= 0); + + assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "2000", STRV_MAKE("2000"), STRV_MAKE("any")) >= 0); + assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "2000", STRV_MAKE("IPv6:2001-2002"), STRV_MAKE("any")) >= 0); + assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("IPv4:6666", "6667"), STRV_MAKE("any")) >= 0); + assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("6667", "6668", ""), STRV_MAKE("any")) >= 0); + assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "7777", STRV_MAKE_EMPTY, STRV_MAKE_EMPTY) >= 0); + assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "8888", STRV_MAKE("any"), STRV_MAKE("any")) >= 0); + + return 0; +} From dcf4781caf830a9f0e393c95658893ca8680a101 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Wed, 18 Nov 2020 00:31:55 -0800 Subject: [PATCH 12/16] dbus: add dbus-cgroup for SocketBind{Allow|Deny}= --- man/org.freedesktop.systemd1.xml | 72 ++++++++++++++++++++ src/core/dbus-cgroup.c | 112 +++++++++++++++++++++++++++++++ src/shared/bus-unit-util.c | 51 ++++++++++++++ 3 files changed, 235 insertions(+) diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index 7a44c9be9b..3d6f1a39b3 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -2482,6 +2482,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { readonly s ManagedOOMPreference = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly a(ss) BPFProgram = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindAllow = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindDeny = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as Environment = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -3018,6 +3022,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + + + @@ -3578,6 +3586,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + + + @@ -4265,6 +4277,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { readonly s ManagedOOMPreference = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly a(ss) BPFProgram = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindAllow = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindDeny = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as Environment = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -4827,6 +4843,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + @@ -5383,6 +5403,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + + + @@ -5972,6 +5996,10 @@ node /org/freedesktop/systemd1/unit/home_2emount { readonly s ManagedOOMPreference = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly a(ss) BPFProgram = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindAllow = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindDeny = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as Environment = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -6462,6 +6490,10 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + + + @@ -6936,6 +6968,10 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + + + @@ -7646,6 +7682,10 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { readonly s ManagedOOMPreference = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly a(ss) BPFProgram = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindAllow = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindDeny = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as Environment = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -8122,6 +8162,10 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + + + @@ -8582,6 +8626,10 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + + + @@ -9145,6 +9193,10 @@ node /org/freedesktop/systemd1/unit/system_2eslice { readonly s ManagedOOMPreference = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly a(ss) BPFProgram = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindAllow = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindDeny = [...]; }; interface org.freedesktop.DBus.Peer { ... }; interface org.freedesktop.DBus.Introspectable { ... }; @@ -9285,6 +9337,10 @@ node /org/freedesktop/systemd1/unit/system_2eslice { + + + + @@ -9429,6 +9485,10 @@ node /org/freedesktop/systemd1/unit/system_2eslice { + + + + @@ -9592,6 +9652,10 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope { readonly s ManagedOOMPreference = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("false") readonly a(ss) BPFProgram = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindAllow = [...]; + @org.freedesktop.DBus.Property.EmitsChangedSignal("false") + readonly a(iqq) SocketBindDeny = [...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly s KillMode = '...'; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") @@ -9748,6 +9812,10 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope { + + + + @@ -9918,6 +9986,10 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope { + + + + diff --git a/src/core/dbus-cgroup.c b/src/core/dbus-cgroup.c index 60a2ad7816..d7f03d0cfd 100644 --- a/src/core/dbus-cgroup.c +++ b/src/core/dbus-cgroup.c @@ -16,6 +16,7 @@ #include "fd-util.h" #include "fileio.h" #include "limits-util.h" +#include "parse-util.h" #include "path-util.h" #include "percent-util.h" @@ -375,6 +376,32 @@ static int property_get_bpf_foreign_program( return sd_bus_message_close_container(reply); } +static int property_get_socket_bind( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + CGroupSocketBindItem **items = userdata, *i; + int r; + + assert(items); + + r = sd_bus_message_open_container(reply, 'a', "(iqq)"); + if (r < 0) + return r; + + LIST_FOREACH(socket_bind_items, i, *items) { + r = sd_bus_message_append(reply, "(iqq)", i->address_family, i->nr_ports, i->port_min); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + const sd_bus_vtable bus_cgroup_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("Delegate", "b", bus_property_get_bool, offsetof(CGroupContext, delegate), 0), @@ -427,6 +454,8 @@ const sd_bus_vtable bus_cgroup_vtable[] = { SD_BUS_PROPERTY("ManagedOOMMemoryPressureLimit", "u", NULL, offsetof(CGroupContext, moom_mem_pressure_limit), 0), SD_BUS_PROPERTY("ManagedOOMPreference", "s", property_get_managed_oom_preference, offsetof(CGroupContext, moom_preference), 0), SD_BUS_PROPERTY("BPFProgram", "a(ss)", property_get_bpf_foreign_program, 0, 0), + SD_BUS_PROPERTY("SocketBindAllow", "a(iqq)", property_get_socket_bind, offsetof(CGroupContext, socket_bind_allow), 0), + SD_BUS_PROPERTY("SocketBindDeny", "a(iqq)", property_get_socket_bind, offsetof(CGroupContext, socket_bind_deny), 0), SD_BUS_VTABLE_END }; @@ -1849,6 +1878,89 @@ int bus_cgroup_set_property( return 1; } + if (STR_IN_SET(name, "SocketBindAllow", "SocketBindDeny")) { + CGroupSocketBindItem **list; + uint16_t nr_ports, port_min; + size_t n = 0; + int family; + + list = streq(name, "SocketBindAllow") ? &c->socket_bind_allow : &c->socket_bind_deny; + + r = sd_bus_message_enter_container(message, 'a', "(iqq)"); + if (r < 0) + return r; + + while ((r = sd_bus_message_read(message, "(iqq)", &family, &nr_ports, &port_min)) > 0) { + + if (!IN_SET(family, AF_UNSPEC, AF_INET, AF_INET6)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "%s= expects INET or INET6 family, if specified.", name); + + if (port_min + (uint32_t) nr_ports > (1 << 16)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "%s= expects maximum port value lesser than 65536.", name); + + if (port_min == 0 && nr_ports != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "%s= expects port range starting with positive value.", name); + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + _cleanup_free_ CGroupSocketBindItem *item = NULL; + + item = new(CGroupSocketBindItem, 1); + if (!item) + return log_oom(); + + *item = (CGroupSocketBindItem) { + .address_family = family, + .nr_ports = nr_ports, + .port_min = port_min + }; + + LIST_PREPEND(socket_bind_items, *list, TAKE_PTR(item)); + } + n++; + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!UNIT_WRITE_FLAGS_NOOP(flags)) { + _cleanup_free_ char *buf = NULL; + _cleanup_fclose_ FILE *f = NULL; + CGroupSocketBindItem *item; + size_t size = 0; + + if (n == 0) + cgroup_context_remove_socket_bind(list); + else { + if ((u->manager->cgroup_supported & CGROUP_MASK_BPF_SOCKET_BIND) == 0) + log_full(LOG_DEBUG, + "Unit %s configures source compiled BPF programs " + "but the local system does not support that.\n" + "Starting this unit will fail!", u->id); + } + + f = open_memstream_unlocked(&buf, &size); + if (!f) + return -ENOMEM; + + fprintf(f, "%s:", name); + + LIST_FOREACH(socket_bind_items, item, *list) + cgroup_context_dump_socket_bind_item(item, f); + + fputc('\n', f); + + r = fflush_and_check(f); + if (r < 0) + return r; + + unit_write_setting(u, flags, name, buf); + } + + return 1; + } if (streq(name, "DisableControllers") || (u->transient && u->load_state == UNIT_STUB)) return bus_cgroup_set_transient_property(u, c, name, message, flags, error); diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index eb28c35924..20d368efca 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -862,6 +862,57 @@ static int bus_append_cgroup_property(sd_bus_message *m, const char *field, cons return 1; } + if (STR_IN_SET(field, "SocketBindAllow", + "SocketBindDeny")) { + if (isempty(eq)) + r = sd_bus_message_append(m, "(sv)", field, "a(iqq)", 0); + else { + const char *address_family, *user_port; + _cleanup_free_ char *word = NULL; + int family = AF_UNSPEC; + + r = extract_first_word(&eq, &word, ":", 0); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Failed to parse %s: %m", field); + + address_family = eq ? word : NULL; + if (address_family) { + if (!STR_IN_SET(address_family, "IPv4", "IPv6")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Only IPv4 and IPv6 protocols are supported"); + + if (streq(address_family, "IPv4")) + family = AF_INET; + else + family = AF_INET6; + } + + user_port = eq ? eq : word; + if (streq(user_port, "any")) { + r = sd_bus_message_append(m, "(sv)", field, "a(iqq)", 1, family, 0, 0); + if (r < 0) + return bus_log_create_error(r); + } else { + uint16_t port_min, port_max; + + r = parse_ip_port_range(user_port, &port_min, &port_max); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_error_errno(r, "Invalid port or port range: %s", user_port); + + r = sd_bus_message_append( + m, "(sv)", field, "a(iqq)", 1, family, port_max - port_min + 1, port_min); + } + } + if (r < 0) + return bus_log_create_error(r); + + return 1; + } + return 0; } From 18ef723ef6535c4a370153a1206859f5a75fb417 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Tue, 20 Apr 2021 16:33:29 -0700 Subject: [PATCH 13/16] systemctl: show SocketBind{Allow|Deny} properties --- src/systemctl/systemctl-show.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c index 1a0bd35617..6d01a6be86 100644 --- a/src/systemctl/systemctl-show.c +++ b/src/systemctl/systemctl-show.c @@ -1710,6 +1710,34 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m if (r < 0) return bus_log_parse_error(r); + return 1; + } else if (STR_IN_SET(name, "SocketBindAllow", "SocketBindDeny")) { + uint16_t nr_ports, port_min; + const char *family; + int af; + + r = sd_bus_message_enter_container(m, SD_BUS_TYPE_ARRAY, "(iqq)"); + if (r < 0) + return bus_log_parse_error(r); + while ((r = sd_bus_message_read(m, "(iqq)", &af, &nr_ports, &port_min)) > 0) { + family = af == AF_INET ? "IPv4:" : af == AF_INET6 ? "IPv6:" : ""; + if (nr_ports == 0) + bus_print_property_valuef(name, expected_value, value, "%sany", family); + else if (nr_ports == 1) + bus_print_property_valuef( + name, expected_value, value, "%s%hu", family, port_min); + else + bus_print_property_valuef( + name, expected_value, value, "%s%hu-%hu", family, port_min, + (uint16_t) (port_min + nr_ports - 1)); + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(m); + if (r < 0) + return bus_log_parse_error(r); + return 1; } From 6359811021f113e8ba782be3fbdfa0b587a7bebd Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Wed, 18 Nov 2020 00:32:14 -0800 Subject: [PATCH 14/16] man: add SocketBind{Allow|Deny}= documentation --- man/systemd.resource-control.xml | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/man/systemd.resource-control.xml b/man/systemd.resource-control.xml index a2d01f7afb..d9b570e232 100644 --- a/man/systemd.resource-control.xml +++ b/man/systemd.resource-control.xml @@ -762,6 +762,76 @@ BPFProgram=bind6:/sys/fs/bpf/sock-addr-hook + + SocketBindAllow=bind-rule + SocketBindDeny=bind-rule + + + Allow or deny binding a socket address to a socket by matching it with the bind-rule and + applying a corresponding action if there is a match. + + bind-rule describes socket properties such as address-family + and ip-ports. + + bind-rule := [address-family:]ip-ports + + address-family := { IPv4 | IPv6 } + + ip-ports := { ip-port | ip-port-range | + any } + + An optional address-family expects IPv4 or IPv6 values. + If not specified, a rule will be matched for both IPv4 and IPv6 addresses and applied depending on other socket fields, e.g. + ip-port. + + ip-port value must lie within 1…65535 interval inclusively, i.e. + dynamic port 0 is not allowed. A range of sequential ports is described by + ip-port-range := ip-port-low-ip-port-high, + where ip-port-low is smaller than or equal to ip-port-high + and both are within 1…65535 inclusively. A special value any + should be used to apply a rule to any port with a positive value. + + To allow multiple rules assign SocketBindAllow= or SocketBindDeny= multiple times. + To clear the existing assignments pass an empty SocketBindAllow= or SocketBindDeny= + assignment. + + For each of SocketBindAllow= and SocketBindDeny=, maximum allowed number of assignments is + 128. + + + Binding to a socket is allowed when a socket address matches an entry in the + SocketBindAllow= list. + + Otherwise, binding is denied when the socket address matches an entry in the + SocketBindDeny= list. + + Otherwise, binding is allowed. + + + The feature is implemented with cgroup/bind4 and cgroup/bind6 cgroup-bpf hooks. + Examples:… +# Allow binding IPv6 socket addresses with a port greater than or equal to 10000. +[Service] +SocketBindAllow=IPv6:10000-65535 +SocketBindDeny=any +… +# Allow binding IPv4 and IPv6 socket addresses with 1234 and 4321 ports. +[Service] +SocketBindAllow=1234 +SocketBindAllow=4321 +SocketBindDeny=any +… +# Deny binding IPv6 socket addresses. +[Service] +SocketBindDeny=IPv6:any +… +# Deny binding IPv4 and IPv6 socket addresses. +[Service] +SocketBindDeny=any +… + + + DeviceAllow= From c3080258750b0eee612c24d4d4bffe42473ce05b Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Fri, 27 Nov 2020 17:54:02 -0800 Subject: [PATCH 15/16] readme: update README with requirements for bpf --- README | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README b/README index e0b7fbf8bf..955dc11e1b 100644 --- a/README +++ b/README @@ -35,6 +35,10 @@ LICENSE: REQUIREMENTS: Linux kernel >= 3.13 Linux kernel >= 4.2 for unified cgroup hierarchy support + Linux kernel >= 4.10 for cgroup-bpf egress and ingress hooks + Linux kernel >= 4.15 for cgroup-bpf device hook + Linux kernel >= 4.17 for cgroup-bpf socket address hooks + Linux kernel >= 5.3 for bounded-loops in BPF program Linux kernel >= 5.4 for signed Verity images support Kernel Config Options: @@ -95,8 +99,20 @@ REQUIREMENTS: Required for CPUQuota= in resource control unit settings CONFIG_CFS_BANDWIDTH - Required for IPAddressDeny= and IPAddressAllow= in resource control + Required for IPAddressDeny=, IPAddressAllow=, IPIngressFilterPath=, + IPEgressFilterPath= in resource control unit settings unit settings + CONFIG_BPF + CONFIG_BPF_SYSCALL + CONFIG_BPF_JIT + CONFIG_HAVE_EBPF_JIT + CONFIG_CGROUP_BPF + + Required for SocketBind{Allow|Deny}= in resource control unit settings + CONFIG_BPF + CONFIG_BPF_SYSCALL + CONFIG_BPF_JIT + CONFIG_HAVE_EBPF_JIT CONFIG_CGROUP_BPF For UEFI systems: @@ -154,6 +170,7 @@ REQUIREMENTS: libcryptsetup (optional), >= 2.3.0 required for signed Verity images support libaudit (optional) libacl (optional) + libbpf >= 0.2.0 (optional) libfdisk >= 2.33 (from util-linux) (optional) libselinux (optional) liblzma (optional) @@ -178,6 +195,8 @@ REQUIREMENTS: meson >= 0.46 (>= 0.49 is required to build position-independent executables) ninja gcc, awk, sed, grep, m4, and similar tools + clang >= 10.0, llvm >= 10.0 (optional, required to build BPF programs + from source code in C) During runtime, you need the following additional dependencies: From 7dc1707aabc3a788f15beb2eec1b4dcfaeec2b47 Mon Sep 17 00:00:00 2001 From: Julia Kartseva Date: Thu, 11 Feb 2021 23:15:17 -0800 Subject: [PATCH 16/16] fuzz: add SocketBind{Allow|Deny}= directive --- test/fuzz/fuzz-unit-file/directives.mount | 2 ++ test/fuzz/fuzz-unit-file/directives.scope | 2 ++ test/fuzz/fuzz-unit-file/directives.service | 2 ++ test/fuzz/fuzz-unit-file/directives.slice | 2 ++ test/fuzz/fuzz-unit-file/directives.socket | 2 ++ test/fuzz/fuzz-unit-file/directives.swap | 2 ++ 6 files changed, 12 insertions(+) diff --git a/test/fuzz/fuzz-unit-file/directives.mount b/test/fuzz/fuzz-unit-file/directives.mount index f1a8d19d18..451f291988 100644 --- a/test/fuzz/fuzz-unit-file/directives.mount +++ b/test/fuzz/fuzz-unit-file/directives.mount @@ -162,6 +162,8 @@ SetCredential= Slice= SloppyOptions= SmackProcessLabel= +SocketBindAllow= +SocketBindDeny= StandardError= StandardInput= StandardInputData= diff --git a/test/fuzz/fuzz-unit-file/directives.scope b/test/fuzz/fuzz-unit-file/directives.scope index 97ca9b47aa..7e69cf816b 100644 --- a/test/fuzz/fuzz-unit-file/directives.scope +++ b/test/fuzz/fuzz-unit-file/directives.scope @@ -52,6 +52,8 @@ RuntimeMaxSec= SendSIGHUP= SendSIGKILL= Slice= +SocketBindAllow= +SocketBindDeny= StartupBlockIOWeight= StartupCPUShares= StartupCPUWeight= diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service index c2069c1a78..9fc83f04d2 100644 --- a/test/fuzz/fuzz-unit-file/directives.service +++ b/test/fuzz/fuzz-unit-file/directives.service @@ -88,6 +88,8 @@ RequiresMountsFor= RequiresOverridable= Requisite= RequisiteOverridable= +SocketBindAllow= +SocketBindDeny= SourcePath= StartLimitAction= StartLimitBurst= diff --git a/test/fuzz/fuzz-unit-file/directives.slice b/test/fuzz/fuzz-unit-file/directives.slice index b96d0628ad..789ac8f0db 100644 --- a/test/fuzz/fuzz-unit-file/directives.slice +++ b/test/fuzz/fuzz-unit-file/directives.slice @@ -45,6 +45,8 @@ MemoryMin= MemorySwapMax= NetClass= Slice= +SocketBindAllow= +SocketBindDeny= StartupBlockIOWeight= StartupCPUShares= StartupCPUWeight= diff --git a/test/fuzz/fuzz-unit-file/directives.socket b/test/fuzz/fuzz-unit-file/directives.socket index 79e04a2e84..11f589e22c 100644 --- a/test/fuzz/fuzz-unit-file/directives.socket +++ b/test/fuzz/fuzz-unit-file/directives.socket @@ -204,6 +204,8 @@ SmackLabel= SmackLabelIPIn= SmackLabelIPOut= SmackProcessLabel= +SocketBindAllow= +SocketBindDeny= SocketGroup= SocketMode= SocketProtocol= diff --git a/test/fuzz/fuzz-unit-file/directives.swap b/test/fuzz/fuzz-unit-file/directives.swap index c3b63aac56..582a136531 100644 --- a/test/fuzz/fuzz-unit-file/directives.swap +++ b/test/fuzz/fuzz-unit-file/directives.swap @@ -158,6 +158,8 @@ SendSIGKILL= SetCredential= Slice= SmackProcessLabel= +SocketBindAllow= +SocketBindDeny= StandardError= StandardInput= StandardInputData=