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:
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/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=
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')
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/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/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";
diff --git a/src/core/cgroup.c b/src/core/cgroup.c
index 8b5df7610c..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"
@@ -200,6 +201,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 +234,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 +392,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 +579,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) {
@@ -1055,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;
@@ -1483,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) {
@@ -1526,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;
@@ -1580,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;
}
@@ -3063,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/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);
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/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);
diff --git a/src/core/meson.build b/src/core/meson.build
index a1294f3a72..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
@@ -123,6 +125,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',
@@ -152,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..ed6c36c11f
--- /dev/null
+++ b/src/core/socket-bind.c
@@ -0,0 +1,238 @@
+/* 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_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;
+ _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;
+}
+
+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
new file mode 100644
index 0000000000..2a6e71a9b9
--- /dev/null
+++ b/src/core/socket-bind.h
@@ -0,0 +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 864bcd3d6e..6ef90eea0e 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,13 @@ 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);
+#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..5bc1237ce0 100644
--- a/src/core/unit.h
+++ b/src/core/unit.h
@@ -309,6 +309,15 @@ 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
+ * 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
diff --git a/src/shared/bpf-link.c b/src/shared/bpf-link.c
new file mode 100644
index 0000000000..f718cb7d2d
--- /dev/null
+++ b/src/shared/bpf-link.c
@@ -0,0 +1,38 @@
+/* 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;
+
+ 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;
+}
+
+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);
+
+ return NULL;
+}
diff --git a/src/shared/bpf-link.h b/src/shared/bpf-link.h
new file mode 100644
index 0000000000..095465b07c
--- /dev/null
+++ b/src/shared/bpf-link.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#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);
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;
}
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,
diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c
index dc45de4b3d..37b1672d5d 100644
--- a/src/systemctl/systemctl-show.c
+++ b/src/systemctl/systemctl-show.c
@@ -1712,6 +1712,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;
}
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-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");
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;
+}
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=
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)