diff --git a/README b/README
index a8f23a0d5b..11487420f7 100644
--- a/README
+++ b/README
@@ -40,6 +40,7 @@ REQUIREMENTS:
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
+ Linux kernel >= 5.7 for BPF links
Kernel Config Options:
CONFIG_DEVTMPFS
@@ -108,7 +109,8 @@ REQUIREMENTS:
CONFIG_HAVE_EBPF_JIT
CONFIG_CGROUP_BPF
- Required for SocketBind{Allow|Deny}= in resource control unit settings
+ Required for SocketBind{Allow|Deny}=, RestrictNetworkInterfaces= in
+ resource control unit settings
CONFIG_BPF
CONFIG_BPF_SYSCALL
CONFIG_BPF_JIT
diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml
index 72bdf4de8f..09cb233553 100644
--- a/man/org.freedesktop.systemd1.xml
+++ b/man/org.freedesktop.systemd1.xml
@@ -2528,6 +2528,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
readonly a(iiqq) SocketBindAllow = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly a(iiqq) SocketBindDeny = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly (bas) RestrictNetworkInterfaces = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -3070,6 +3072,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -3638,6 +3642,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
+
+
@@ -4340,6 +4346,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
readonly a(iiqq) SocketBindAllow = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly a(iiqq) SocketBindDeny = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly (bas) RestrictNetworkInterfaces = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -4910,6 +4918,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -5476,6 +5486,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
+
+
@@ -6075,6 +6087,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
readonly a(iiqq) SocketBindAllow = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly a(iiqq) SocketBindDeny = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly (bas) RestrictNetworkInterfaces = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -6573,6 +6587,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -7057,6 +7073,8 @@ node /org/freedesktop/systemd1/unit/home_2emount {
+
+
@@ -7777,6 +7795,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
readonly a(iiqq) SocketBindAllow = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly a(iiqq) SocketBindDeny = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly (bas) RestrictNetworkInterfaces = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly as Environment = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -8261,6 +8281,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -8731,6 +8753,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
+
+
@@ -9304,6 +9328,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
readonly a(iiqq) SocketBindAllow = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly a(iiqq) SocketBindDeny = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly (bas) RestrictNetworkInterfaces = ...;
};
interface org.freedesktop.DBus.Peer { ... };
interface org.freedesktop.DBus.Introspectable { ... };
@@ -9448,6 +9474,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
+
+
@@ -9598,6 +9626,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
+
+
@@ -9767,6 +9797,8 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope {
readonly a(iiqq) SocketBindAllow = [...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
readonly a(iiqq) SocketBindDeny = [...];
+ @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
+ readonly (bas) RestrictNetworkInterfaces = ...;
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s KillMode = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -9927,6 +9959,8 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope {
+
+
@@ -10103,6 +10137,8 @@ node /org/freedesktop/systemd1/unit/session_2d1_2escope {
+
+
diff --git a/man/systemd.resource-control.xml b/man/systemd.resource-control.xml
index f5cbe688ee..15b7f314a3 100644
--- a/man/systemd.resource-control.xml
+++ b/man/systemd.resource-control.xml
@@ -855,6 +855,52 @@ SocketBindDeny=any
+
+ RestrictNetworkInterfaces=
+
+
+ Takes a list of space-separated network interface names. This option restricts the network
+ interfaces that processes of this unit can use. By default processes can only use the network interfaces
+ listed (allow-list). If the first character of the rule is ~, the effect is inverted:
+ the processes can only use network interfaces not listed (deny-list).
+
+
+ This option can appear multiple times, in which case the network interface names are merged. If the
+ empty string is assigned the set is reset, all prior assigments will have not effect.
+
+
+ If you specify both types of this option (i.e. allow-listing and deny-listing), the first encountered
+ will take precedence and will dictate the default action (allow vs deny). Then the next occurrences of this
+ option will add or delete the listed network interface names from the set, depending of its type and the
+ default action.
+
+
+ The loopback interface ("lo") is not treated in any special way, you have to configure it explicitly
+ in the unit file.
+
+ Example 1: allow-list
+
+RestrictNetworkInterfaces=eth1
+RestrictNetworkInterfaces=eth2
+ Programs in the unit will be only able to use the eth1 and eth2 network
+ interfaces.
+
+
+ Example 2: deny-list
+
+RestrictNetworkInterfaces=~eth1 eth2
+ Programs in the unit will be able to use any network interface but eth1 and eth2.
+
+
+ Example 3: mixed
+
+RestrictNetworkInterfaces=eth1 eth2
+RestrictNetworkInterfaces=~eth1
+ Programs in the unit will be only able to use the eth2 network interface.
+
+
+
+
DeviceAllow=
diff --git a/src/basic/cgroup-util.c b/src/basic/cgroup-util.c
index 5bf7c03c00..95891f68aa 100644
--- a/src/basic/cgroup-util.c
+++ b/src/basic/cgroup-util.c
@@ -2214,6 +2214,7 @@ static const char *const cgroup_controller_table[_CGROUP_CONTROLLER_MAX] = {
[CGROUP_CONTROLLER_BPF_DEVICES] = "bpf-devices",
[CGROUP_CONTROLLER_BPF_FOREIGN] = "bpf-foreign",
[CGROUP_CONTROLLER_BPF_SOCKET_BIND] = "bpf-socket-bind",
+ [CGROUP_CONTROLLER_BPF_RESTRICT_NETWORK_INTERFACES] = "bpf-restrict-network-interfaces",
};
DEFINE_STRING_TABLE_LOOKUP(cgroup_controller, CGroupController);
diff --git a/src/basic/cgroup-util.h b/src/basic/cgroup-util.h
index b2dd9b56a5..90ccd2c032 100644
--- a/src/basic/cgroup-util.h
+++ b/src/basic/cgroup-util.h
@@ -32,6 +32,7 @@ typedef enum CGroupController {
CGROUP_CONTROLLER_BPF_DEVICES,
CGROUP_CONTROLLER_BPF_FOREIGN,
CGROUP_CONTROLLER_BPF_SOCKET_BIND,
+ CGROUP_CONTROLLER_BPF_RESTRICT_NETWORK_INTERFACES,
_CGROUP_CONTROLLER_MAX,
_CGROUP_CONTROLLER_INVALID = -EINVAL,
@@ -53,6 +54,7 @@ typedef enum CGroupMask {
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),
+ CGROUP_MASK_BPF_RESTRICT_NETWORK_INTERFACES = CGROUP_CONTROLLER_TO_MASK(CGROUP_CONTROLLER_BPF_RESTRICT_NETWORK_INTERFACES),
/* 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,
@@ -61,7 +63,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_SOCKET_BIND,
+ CGROUP_MASK_BPF = CGROUP_MASK_BPF_FIREWALL|CGROUP_MASK_BPF_DEVICES|CGROUP_MASK_BPF_FOREIGN|CGROUP_MASK_BPF_SOCKET_BIND|CGROUP_MASK_BPF_RESTRICT_NETWORK_INTERFACES,
_CGROUP_MASK_ALL = CGROUP_CONTROLLER_TO_MASK(_CGROUP_CONTROLLER_MAX) - 1
} CGroupMask;
diff --git a/src/core/bpf/restrict_ifaces/meson.build b/src/core/bpf/restrict_ifaces/meson.build
new file mode 100644
index 0000000000..5bf024d42d
--- /dev/null
+++ b/src/core/bpf/restrict_ifaces/meson.build
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+if conf.get('BPF_FRAMEWORK') == 1
+ restrict_ifaces_skel_h = custom_target(
+ 'restrict-ifaces.skel.h',
+ input : 'restrict-ifaces.bpf.c',
+ output : 'restrict-ifaces.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/restrict_ifaces/restrict-ifaces.bpf.c b/src/core/bpf/restrict_ifaces/restrict-ifaces.bpf.c
new file mode 100644
index 0000000000..347a3a8d21
--- /dev/null
+++ b/src/core/bpf/restrict_ifaces/restrict-ifaces.bpf.c
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/* must precede due to integer types
+ * in bpf helpers signatures.
+ */
+#include
+#include
+
+const volatile __u8 is_allow_list = 0;
+
+/* Map containing the network interfaces indexes.
+ * The interpretation of the map depends on the value of is_allow_list.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __type(key, __u32);
+ __type(value, __u8);
+} sd_restrictif SEC(".maps");
+
+#define DROP 0
+#define PASS 1
+
+static inline int restrict_network_interfaces_impl(const struct __sk_buff *sk) {
+ __u32 zero = 0, ifindex;
+ __u8 *lookup_result;
+
+ ifindex = sk->ifindex;
+ lookup_result = bpf_map_lookup_elem(&sd_restrictif, &ifindex);
+ if (is_allow_list) {
+ /* allow-list: let the packet pass if iface in the list */
+ if (lookup_result)
+ return PASS;
+ } else {
+ /* deny-list: let the packet pass if iface *not* in the list */
+ if (!lookup_result)
+ return PASS;
+ }
+
+ return DROP;
+}
+
+SEC("cgroup_skb/egress")
+int sd_restrictif_e(const struct __sk_buff *sk) {
+ return restrict_network_interfaces_impl(sk);
+}
+
+SEC("cgroup_skb/ingress")
+int sd_restrictif_i(const struct __sk_buff *sk) {
+ return restrict_network_interfaces_impl(sk);
+}
+
+static const char _license[] SEC("license") = "LGPL-2.1-or-later";
diff --git a/src/core/cgroup.c b/src/core/cgroup.c
index bbeb5b2f50..5b612ee22b 100644
--- a/src/core/cgroup.c
+++ b/src/core/cgroup.c
@@ -28,6 +28,7 @@
#include "percent-util.h"
#include "process-util.h"
#include "procfs-util.h"
+#include "restrict-ifaces.h"
#include "special.h"
#include "stat-util.h"
#include "stdio-util.h"
@@ -246,6 +247,8 @@ void cgroup_context_done(CGroupContext *c) {
while (c->bpf_foreign_programs)
cgroup_context_remove_bpf_foreign_program(c, c->bpf_foreign_programs);
+ c->restrict_network_interfaces = set_free(c->restrict_network_interfaces);
+
cpu_set_reset(&c->cpuset_cpus);
cpu_set_reset(&c->cpuset_mems);
}
@@ -583,6 +586,12 @@ void cgroup_context_dump(Unit *u, FILE* f, const char *prefix) {
cgroup_context_dump_socket_bind_item(bi, f);
fputc('\n', f);
}
+
+ if (c->restrict_network_interfaces) {
+ char *iface;
+ SET_FOREACH(iface, c->restrict_network_interfaces)
+ fprintf(f, "%sRestrictNetworkInterfaces: %s\n", prefix, iface);
+ }
}
void cgroup_context_dump_socket_bind_item(const CGroupSocketBindItem *item, FILE *f) {
@@ -1097,6 +1106,12 @@ static void cgroup_apply_socket_bind(Unit *u) {
(void) bpf_socket_bind_install(u);
}
+static void cgroup_apply_restrict_network_interfaces(Unit *u) {
+ assert(u);
+
+ (void) restrict_network_interfaces_install(u);
+}
+
static int cgroup_apply_devices(Unit *u) {
_cleanup_(bpf_program_unrefp) BPFProgram *prog = NULL;
const char *path;
@@ -1527,6 +1542,9 @@ static void cgroup_context_apply(
if (apply_mask & CGROUP_MASK_BPF_SOCKET_BIND)
cgroup_apply_socket_bind(u);
+
+ if (apply_mask & CGROUP_MASK_BPF_RESTRICT_NETWORK_INTERFACES)
+ cgroup_apply_restrict_network_interfaces(u);
}
static bool unit_get_needs_bpf_firewall(Unit *u) {
@@ -1580,6 +1598,17 @@ static bool unit_get_needs_socket_bind(Unit *u) {
return c->socket_bind_allow || c->socket_bind_deny;
}
+static bool unit_get_needs_restrict_network_interfaces(Unit *u) {
+ CGroupContext *c;
+ assert(u);
+
+ c = unit_get_cgroup_context(u);
+ if (!c)
+ return false;
+
+ return !set_isempty(c->restrict_network_interfaces);
+}
+
static CGroupMask unit_get_cgroup_mask(Unit *u) {
CGroupMask mask = 0;
CGroupContext *c;
@@ -1635,6 +1664,9 @@ static CGroupMask unit_get_bpf_mask(Unit *u) {
if (unit_get_needs_socket_bind(u))
mask |= CGROUP_MASK_BPF_SOCKET_BIND;
+ if (unit_get_needs_restrict_network_interfaces(u))
+ mask |= CGROUP_MASK_BPF_RESTRICT_NETWORK_INTERFACES;
+
return mask;
}
@@ -3132,6 +3164,11 @@ static int cg_bpf_mask_supported(CGroupMask *ret) {
if (r > 0)
mask |= CGROUP_MASK_BPF_SOCKET_BIND;
+ /* BPF-based cgroup_skb/{egress|ingress} hooks */
+ r = restrict_network_interfaces_supported();
+ if (r > 0)
+ mask |= CGROUP_MASK_BPF_RESTRICT_NETWORK_INTERFACES;
+
*ret = mask;
return 0;
}
diff --git a/src/core/cgroup.h b/src/core/cgroup.h
index 3f8cad899d..99bf7e22d8 100644
--- a/src/core/cgroup.h
+++ b/src/core/cgroup.h
@@ -160,6 +160,9 @@ struct CGroupContext {
char **ip_filters_egress;
LIST_HEAD(CGroupBPFForeignProgram, bpf_foreign_programs);
+ Set *restrict_network_interfaces;
+ bool restrict_network_interfaces_is_allow_list;
+
/* For legacy hierarchies */
uint64_t cpu_shares;
uint64_t startup_cpu_shares;
diff --git a/src/core/dbus-cgroup.c b/src/core/dbus-cgroup.c
index 77fbca8389..75efa9fe1f 100644
--- a/src/core/dbus-cgroup.c
+++ b/src/core/dbus-cgroup.c
@@ -20,6 +20,7 @@
#include "parse-util.h"
#include "path-util.h"
#include "percent-util.h"
+#include "socket-util.h"
BUS_DEFINE_PROPERTY_GET(bus_property_get_tasks_max, "t", TasksMax, tasks_max_resolve);
@@ -403,6 +404,47 @@ static int property_get_socket_bind(
return sd_bus_message_close_container(reply);
}
+static int property_get_restrict_network_interfaces(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+ int r;
+ CGroupContext *c = userdata;
+ char *iface;
+
+ assert(bus);
+ assert(reply);
+ assert(c);
+
+ r = sd_bus_message_open_container(reply, 'r', "bas");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "b", c->restrict_network_interfaces_is_allow_list);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "s");
+ if (r < 0)
+ return r;
+
+ SET_FOREACH(iface, c->restrict_network_interfaces) {
+ r = sd_bus_message_append(reply, "s", iface);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ 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),
@@ -457,6 +499,7 @@ const sd_bus_vtable bus_cgroup_vtable[] = {
SD_BUS_PROPERTY("BPFProgram", "a(ss)", property_get_bpf_foreign_program, 0, 0),
SD_BUS_PROPERTY("SocketBindAllow", "a(iiqq)", property_get_socket_bind, offsetof(CGroupContext, socket_bind_allow), 0),
SD_BUS_PROPERTY("SocketBindDeny", "a(iiqq)", property_get_socket_bind, offsetof(CGroupContext, socket_bind_deny), 0),
+ SD_BUS_PROPERTY("RestrictNetworkInterfaces", "(bas)", property_get_restrict_network_interfaces, 0, 0),
SD_BUS_VTABLE_END
};
@@ -1963,6 +2006,64 @@ int bus_cgroup_set_property(
return 1;
}
+ if (streq(name, "RestrictNetworkInterfaces")) {
+ int is_allow_list;
+ _cleanup_strv_free_ char **l = NULL;
+
+ r = sd_bus_message_enter_container(message, 'r', "bas");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "b", &is_allow_list);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read_strv(message, &l);
+ 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 *joined = NULL;
+ char **s;
+
+ if (strv_isempty(l)) {
+ c->restrict_network_interfaces_is_allow_list = false;
+ c->restrict_network_interfaces = set_free(c->restrict_network_interfaces);
+
+ unit_write_settingf(u, flags, name, "%s=", name);
+ return 1;
+ }
+
+ if (set_isempty(c->restrict_network_interfaces))
+ c->restrict_network_interfaces_is_allow_list = is_allow_list;
+
+ STRV_FOREACH(s, l) {
+ if (!ifname_valid(*s)) {
+ log_full(LOG_WARNING, "Invalid interface name, ignoring: %s", *s);
+ continue;
+ }
+ if (c->restrict_network_interfaces_is_allow_list != (bool) is_allow_list)
+ free(set_remove(c->restrict_network_interfaces, *s));
+ else {
+ r = set_put_strdup(&c->restrict_network_interfaces, *s);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ joined = strv_join(l, " ");
+ if (!joined)
+ return -ENOMEM;
+
+ unit_write_settingf(u, flags, name, "%s=%s%s", name, is_allow_list ? "" : "~", joined);
+ }
+
+ 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.in b/src/core/load-fragment-gperf.gperf.in
index 96507907f6..489841af7a 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -233,6 +233,7 @@
{{type}}.BPFProgram, config_parse_bpf_foreign_program, 0, offsetof({{type}}, cgroup_context)
{{type}}.SocketBindAllow, config_parse_cgroup_socket_bind, 0, offsetof({{type}}, cgroup_context.socket_bind_allow)
{{type}}.SocketBindDeny, config_parse_cgroup_socket_bind, 0, offsetof({{type}}, cgroup_context.socket_bind_deny)
+{{type}}.RestrictNetworkInterfaces, config_parse_restrict_network_interfaces, 0, offsetof({{type}}, cgroup_context)
{%- endmacro -%}
%{
diff --git a/src/core/load-fragment.c b/src/core/load-fragment.c
index 6eefaaf27d..92815b1dba 100644
--- a/src/core/load-fragment.c
+++ b/src/core/load-fragment.c
@@ -5711,6 +5711,72 @@ int config_parse_cgroup_socket_bind(
return 0;
}
+int config_parse_restrict_network_interfaces(
+ 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) {
+ CGroupContext *c = data;
+ bool is_allow_rule = true;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(data);
+
+ if (isempty(rvalue)) {
+ /* Empty assignment resets the list */
+ c->restrict_network_interfaces = set_free(c->restrict_network_interfaces);
+ return 0;
+ }
+
+ if (rvalue[0] == '~') {
+ is_allow_rule = false;
+ rvalue++;
+ }
+
+ if (set_isempty(c->restrict_network_interfaces))
+ /* Only initialize this when creating the set */
+ c->restrict_network_interfaces_is_allow_list = is_allow_rule;
+
+ for (const char *p = rvalue;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, NULL, EXTRACT_UNQUOTE);
+ if (r == 0)
+ break;
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Trailing garbage in %s, ignoring: %s", lvalue, rvalue);
+ break;
+ }
+
+ if (!ifname_valid(word)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid interface name, ignoring: %s", word);
+ continue;
+ }
+
+ if (c->restrict_network_interfaces_is_allow_list != is_allow_rule)
+ free(set_remove(c->restrict_network_interfaces, word));
+ else {
+ r = set_put_strdup(&c->restrict_network_interfaces, word);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ 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 45e9c397e4..fe98091ee4 100644
--- a/src/core/load-fragment.h
+++ b/src/core/load-fragment.h
@@ -141,6 +141,7 @@ 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);
+CONFIG_PARSER_PROTOTYPE(config_parse_restrict_network_interfaces);
/* 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 1941081972..489a4d869f 100644
--- a/src/core/meson.build
+++ b/src/core/meson.build
@@ -97,6 +97,8 @@ libcore_sources = '''
namespace.h
path.c
path.h
+ restrict-ifaces.c
+ restrict-ifaces.h
scope.c
scope.h
selinux-access.c
@@ -136,6 +138,11 @@ if conf.get('BPF_FRAMEWORK') == 1
libcore_sources += [socket_bind_skel_h]
endif
+subdir('bpf/restrict_ifaces')
+if conf.get('BPF_FRAMEWORK') == 1
+ libcore_sources += [restrict_ifaces_skel_h]
+endif
+
load_fragment_gperf_gperf = custom_target(
'load-fragment-gperf.gperf',
input : 'load-fragment-gperf.gperf.in',
diff --git a/src/core/restrict-ifaces.c b/src/core/restrict-ifaces.c
new file mode 100644
index 0000000000..709f74929b
--- /dev/null
+++ b/src/core/restrict-ifaces.c
@@ -0,0 +1,205 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "fd-util.h"
+#include "restrict-ifaces.h"
+#include "netlink-util.h"
+
+#if BPF_FRAMEWORK
+/* libbpf, clang and llc compile time dependencies are satisfied */
+
+#include "bpf-dlopen.h"
+#include "bpf-link.h"
+
+#include "bpf/restrict_ifaces/restrict-ifaces.skel.h"
+
+static struct restrict_ifaces_bpf *restrict_ifaces_bpf_free(struct restrict_ifaces_bpf *obj) {
+ restrict_ifaces_bpf__destroy(obj);
+ return NULL;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(struct restrict_ifaces_bpf *, restrict_ifaces_bpf_free);
+
+static int prepare_restrict_ifaces_bpf(Unit* u, bool is_allow_list,
+ const Set *restrict_network_interfaces,
+ struct restrict_ifaces_bpf **ret_object) {
+ _cleanup_(restrict_ifaces_bpf_freep) struct restrict_ifaces_bpf *obj = NULL;
+ _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+ char *iface;
+ int r, map_fd;
+
+ assert(ret_object);
+
+ obj = restrict_ifaces_bpf__open();
+ if (!obj)
+ return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOMEM), "Failed to open BPF object");
+
+ r = sym_bpf_map__resize(obj->maps.sd_restrictif, MAX(set_size(restrict_network_interfaces), 1u));
+ if (r != 0)
+ return log_unit_error_errno(u, r,
+ "Failed to resize BPF map '%s': %m",
+ sym_bpf_map__name(obj->maps.sd_restrictif));
+
+ obj->rodata->is_allow_list = is_allow_list;
+
+ r = restrict_ifaces_bpf__load(obj);
+ if (r != 0)
+ return log_unit_error_errno(u, r, "Failed to load BPF object: %m");
+
+ map_fd = sym_bpf_map__fd(obj->maps.sd_restrictif);
+
+ SET_FOREACH(iface, restrict_network_interfaces) {
+ uint8_t dummy = 0;
+ int ifindex;
+ ifindex = rtnl_resolve_interface(&rtnl, iface);
+ if (ifindex < 0) {
+ log_unit_warning_errno(u, ifindex, "Couldn't find index of network interface: %m. Ignoring '%s'", iface);
+ continue;
+ }
+
+ if (sym_bpf_map_update_elem(map_fd, &ifindex, &dummy, BPF_ANY))
+ return log_unit_error_errno(u, errno, "Failed to update BPF map '%s' fd: %m", sym_bpf_map__name(obj->maps.sd_restrictif));
+ }
+
+ *ret_object = TAKE_PTR(obj);
+ return 0;
+}
+
+int restrict_network_interfaces_supported(void) {
+ _cleanup_(restrict_ifaces_bpf_freep) struct restrict_ifaces_bpf *obj = NULL;
+ int r;
+ static int supported = -1;
+
+ if (supported >= 0)
+ return supported;
+
+ r = cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER);
+ if (r < 0) {
+ log_warning_errno(r, "Can't determine whether the unified hierarchy is used: %m");
+ supported = 0;
+ return supported;
+ }
+ if (r == 0) {
+ log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Not running with unified cgroup hierarchy, BPF is not supported");
+ supported = 0;
+ return supported;
+ }
+
+ if (dlopen_bpf() < 0)
+ return false;
+
+ if (!sym_bpf_probe_prog_type(BPF_PROG_TYPE_CGROUP_SKB, /*ifindex=*/0)) {
+ log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "BPF program type cgroup_skb is not supported");
+ supported = 0;
+ return supported;
+ }
+
+ r = prepare_restrict_ifaces_bpf(NULL, true, NULL, &obj);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to load BPF object: %m");
+
+ supported = bpf_can_link_program(obj->progs.sd_restrictif_i);
+ return supported;
+}
+
+static int restrict_network_interfaces_install_impl(Unit *u) {
+ _cleanup_(bpf_link_freep) struct bpf_link *egress_link = NULL, *ingress_link = NULL;
+ _cleanup_(restrict_ifaces_bpf_freep) struct restrict_ifaces_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->restrict_network_interfaces)
+ return 0;
+
+ r = prepare_restrict_ifaces_bpf(u,
+ cc->restrict_network_interfaces_is_allow_list,
+ cc->restrict_network_interfaces,
+ &obj);
+ if (r < 0)
+ return r;
+
+ cgroup_fd = open(cgroup_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY, 0);
+ if (cgroup_fd < 0)
+ return -errno;
+
+ ingress_link = sym_bpf_program__attach_cgroup(obj->progs.sd_restrictif_i, cgroup_fd);
+ r = sym_libbpf_get_error(ingress_link);
+ if (r != 0)
+ return log_unit_error_errno(u, r, "Failed to create ingress cgroup link: %m");
+
+ egress_link = sym_bpf_program__attach_cgroup(obj->progs.sd_restrictif_e, cgroup_fd);
+ r = sym_libbpf_get_error(egress_link);
+ if (r != 0)
+ return log_unit_error_errno(u, r, "Failed to create egress cgroup link: %m");
+
+ u->restrict_ifaces_ingress_bpf_link = TAKE_PTR(ingress_link);
+ u->restrict_ifaces_egress_bpf_link = TAKE_PTR(egress_link);
+
+ return 0;
+}
+
+int restrict_network_interfaces_install(Unit *u) {
+ int r = restrict_network_interfaces_install_impl(u);
+ fdset_close(u->initial_restric_ifaces_link_fds);
+ return r;
+}
+
+int serialize_restrict_network_interfaces(Unit *u, FILE *f, FDSet *fds) {
+ int r;
+
+ assert(u);
+
+ r = bpf_serialize_link(f, fds, "restrict-ifaces-bpf-fd", u->restrict_ifaces_ingress_bpf_link);
+ if (r < 0)
+ return r;
+
+ return bpf_serialize_link(f, fds, "restrict-ifaces-bpf-fd", u->restrict_ifaces_egress_bpf_link);
+}
+
+int restrict_network_interfaces_add_initial_link_fd(Unit *u, int fd) {
+ int r;
+
+ assert(u);
+
+ if (!u->initial_restric_ifaces_link_fds) {
+ u->initial_restric_ifaces_link_fds = fdset_new();
+ if (!u->initial_restric_ifaces_link_fds)
+ return log_oom();
+ }
+
+ r = fdset_put(u->initial_restric_ifaces_link_fds, fd);
+ if (r < 0)
+ return log_unit_error_errno(u, r, "Failed to put restrict-ifaces-bpf-fd %d to restored fdset: %m", fd);
+
+ return 0;
+}
+
+#else /* ! BPF_FRAMEWORK */
+int restrict_network_interfaces_supported(void) {
+ return 0;
+}
+
+int restrict_network_interfaces_install(Unit *u) {
+ return log_unit_debug_errno(u, SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Failed to install RestrictInterfaces: BPF programs built from source code are not supported: %m");
+}
+
+int serialize_restrict_network_interfaces(Unit *u, FILE *f, FDSet *fds) {
+ return 0;
+}
+
+int restrict_network_interfaces_add_initial_link_fd(Unit *u, int fd) {
+ return 0;
+}
+#endif
diff --git a/src/core/restrict-ifaces.h b/src/core/restrict-ifaces.h
new file mode 100644
index 0000000000..be9e5224c3
--- /dev/null
+++ b/src/core/restrict-ifaces.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "fdset.h"
+#include "unit.h"
+
+typedef struct Unit Unit;
+
+int restrict_network_interfaces_supported(void);
+int restrict_network_interfaces_install(Unit *u);
+
+int serialize_restrict_network_interfaces(Unit *u, FILE *f, FDSet *fds);
+
+/* Add BPF link fd created before daemon-reload or daemon-reexec.
+ * FDs will be closed at the end of restrict_network_interfaces_install. */
+int restrict_network_interfaces_add_initial_link_fd(Unit *u, int fd);
diff --git a/src/core/unit-serialize.c b/src/core/unit-serialize.c
index ab0f03a0ca..9e1664ff53 100644
--- a/src/core/unit-serialize.c
+++ b/src/core/unit-serialize.c
@@ -7,6 +7,7 @@
#include "fileio.h"
#include "format-util.h"
#include "parse-util.h"
+#include "restrict-ifaces.h"
#include "serialize.h"
#include "string-table.h"
#include "unit-serialize.h"
@@ -173,6 +174,8 @@ int unit_serialize(Unit *u, FILE *f, FDSet *fds, bool switching_root) {
(void) bpf_program_serialize_attachment_set(f, fds, "ip-bpf-custom-ingress-installed", u->ip_bpf_custom_ingress_installed);
(void) bpf_program_serialize_attachment_set(f, fds, "ip-bpf-custom-egress-installed", u->ip_bpf_custom_egress_installed);
+ (void) serialize_restrict_network_interfaces(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))
@@ -413,6 +416,21 @@ int unit_deserialize(Unit *u, FILE *f, FDSet *fds) {
(void) bpf_program_deserialize_attachment_set(v, fds, &u->ip_bpf_custom_egress_installed);
continue;
+ } else if (streq(l, "restrict-ifaces-bpf-fd")) {
+ int fd;
+
+ if (safe_atoi(v, &fd) < 0 || fd < 0 || !fdset_contains(fds, fd)) {
+ log_unit_debug(u, "Failed to parse restrict-ifaces-bpf-fd value: %s", v);
+ continue;
+ }
+ if (fdset_remove(fds, fd) < 0) {
+ log_unit_debug(u, "Failed to remove restrict-ifaces-bpf-fd %d from fdset", fd);
+ continue;
+ }
+
+ (void) restrict_network_interfaces_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 2f7f022ef3..7d72dfa864 100644
--- a/src/core/unit.c
+++ b/src/core/unit.c
@@ -766,6 +766,12 @@ Unit* unit_free(Unit *u) {
bpf_program_unref(u->bpf_device_control_installed);
+#if BPF_FRAMEWORK
+ bpf_link_free(u->restrict_ifaces_ingress_bpf_link);
+ bpf_link_free(u->restrict_ifaces_egress_bpf_link);
+#endif
+ fdset_free(u->initial_restric_ifaces_link_fds);
+
condition_free_list(u->conditions);
condition_free_list(u->asserts);
diff --git a/src/core/unit.h b/src/core/unit.h
index 48074d8ca5..b3e9c2106f 100644
--- a/src/core/unit.h
+++ b/src/core/unit.h
@@ -335,6 +335,12 @@ typedef struct Unit {
struct bpf_link *ipv6_socket_bind_link;
#endif
+ FDSet *initial_restric_ifaces_link_fds;
+#if BPF_FRAMEWORK
+ struct bpf_link *restrict_ifaces_ingress_bpf_link;
+ struct bpf_link *restrict_ifaces_egress_bpf_link;
+#endif
+
/* Low-priority event source which is used to remove watched PIDs that have gone away, and subscribe to any new
* ones which might have appeared. */
sd_event_source *rewatch_pids_event_source;
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c
index 1ff1e7600d..669d0df058 100644
--- a/src/shared/bus-unit-util.c
+++ b/src/shared/bus-unit-util.c
@@ -1384,7 +1384,8 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
if (STR_IN_SET(field, "RestrictAddressFamilies",
"SystemCallFilter",
- "SystemCallLog")) {
+ "SystemCallLog",
+ "RestrictNetworkInterfaces")) {
int allow_list = 1;
const char *p = eq;
diff --git a/src/systemctl/systemctl-show.c b/src/systemctl/systemctl-show.c
index b325d44c5e..d008c4172a 100644
--- a/src/systemctl/systemctl-show.c
+++ b/src/systemctl/systemctl-show.c
@@ -1023,7 +1023,7 @@ static int print_property(const char *name, const char *expected_value, sd_bus_m
return 1;
- } else if (STR_IN_SET(name, "SystemCallFilter", "SystemCallLog", "RestrictAddressFamilies")) {
+ } else if (STR_IN_SET(name, "SystemCallFilter", "SystemCallLog", "RestrictAddressFamilies", "RestrictNetworkInterfaces")) {
_cleanup_strv_free_ char **l = NULL;
int allow_list;
diff --git a/src/test/test-cgroup-mask.c b/src/test/test-cgroup-mask.c
index 184b393dd5..415ddf3e98 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 bpf-socket-bind");
+ 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 bpf-restrict-network-interfaces");
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/test/TEST-62-RESTRICT-IFACES/Makefile b/test/TEST-62-RESTRICT-IFACES/Makefile
new file mode 120000
index 0000000000..e9f93b1104
--- /dev/null
+++ b/test/TEST-62-RESTRICT-IFACES/Makefile
@@ -0,0 +1 @@
+../TEST-01-BASIC/Makefile
\ No newline at end of file
diff --git a/test/TEST-62-RESTRICT-IFACES/test.sh b/test/TEST-62-RESTRICT-IFACES/test.sh
new file mode 100755
index 0000000000..b2829d2a1f
--- /dev/null
+++ b/test/TEST-62-RESTRICT-IFACES/test.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+TEST_NO_NSPAWN=1
+
+set -e
+TEST_DESCRIPTION="test RestrictNetworkInterfaces="
+. $TEST_BASE_DIR/test-functions
+
+do_test "$@" 62
diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service
index 3039d1c0cd..494c7545e4 100644
--- a/test/fuzz/fuzz-unit-file/directives-all.service
+++ b/test/fuzz/fuzz-unit-file/directives-all.service
@@ -886,6 +886,7 @@ RemoveIPC=
ReserveVT=
RestrictAddressFamilies=
RestrictNamespaces=
+RestrictNetworkInterfaces=
RestrictRealtime=
RestrictSUIDSGID=
RuntimeDirectory=
diff --git a/test/fuzz/fuzz-unit-file/directives.mount b/test/fuzz/fuzz-unit-file/directives.mount
index d161c81ff6..fd82fc5348 100644
--- a/test/fuzz/fuzz-unit-file/directives.mount
+++ b/test/fuzz/fuzz-unit-file/directives.mount
@@ -144,6 +144,7 @@ RemoveIPC=
RestartKillSignal=
RestrictAddressFamilies=
RestrictNamespaces=
+RestrictNetworkInterfaces=
RestrictRealtime=
RestrictSUIDSGID=
RootDirectory=
diff --git a/test/fuzz/fuzz-unit-file/directives.scope b/test/fuzz/fuzz-unit-file/directives.scope
index 7e69cf816b..ab49083311 100644
--- a/test/fuzz/fuzz-unit-file/directives.scope
+++ b/test/fuzz/fuzz-unit-file/directives.scope
@@ -48,6 +48,7 @@ MemoryMin=
MemorySwapMax=
NetClass=
RestartKillSignal=
+RestrictNetworkInterfaces=
RuntimeMaxSec=
SendSIGHUP=
SendSIGKILL=
diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service
index 35d1f2a104..6a80bbcb2f 100644
--- a/test/fuzz/fuzz-unit-file/directives.service
+++ b/test/fuzz/fuzz-unit-file/directives.service
@@ -275,6 +275,7 @@ RestartPreventExitStatus=
RestartSec=
RestrictAddressFamilies=
RestrictNamespaces=
+RestrictNetworkInterfaces=
RestrictRealtime=
RestrictSUIDSGID=
RootDirectory=
diff --git a/test/fuzz/fuzz-unit-file/directives.slice b/test/fuzz/fuzz-unit-file/directives.slice
index 789ac8f0db..17bd431db7 100644
--- a/test/fuzz/fuzz-unit-file/directives.slice
+++ b/test/fuzz/fuzz-unit-file/directives.slice
@@ -44,6 +44,7 @@ MemoryMax=
MemoryMin=
MemorySwapMax=
NetClass=
+RestrictNetworkInterfaces=
Slice=
SocketBindAllow=
SocketBindDeny=
diff --git a/test/fuzz/fuzz-unit-file/directives.socket b/test/fuzz/fuzz-unit-file/directives.socket
index 1835167cfb..1a79a0dfd1 100644
--- a/test/fuzz/fuzz-unit-file/directives.socket
+++ b/test/fuzz/fuzz-unit-file/directives.socket
@@ -180,6 +180,7 @@ RemoveOnStop=
RestartKillSignal=
RestrictAddressFamilies=
RestrictNamespaces=
+RestrictNetworkInterfaces=
RestrictRealtime=
RestrictSUIDSGID=
ReusePort=
diff --git a/test/fuzz/fuzz-unit-file/directives.swap b/test/fuzz/fuzz-unit-file/directives.swap
index 814d066fac..204e172514 100644
--- a/test/fuzz/fuzz-unit-file/directives.swap
+++ b/test/fuzz/fuzz-unit-file/directives.swap
@@ -141,6 +141,7 @@ RemoveIPC=
RestartKillSignal=
RestrictAddressFamilies=
RestrictNamespaces=
+RestrictNetworkInterfaces=
RestrictRealtime=
RestrictSUIDSGID=
RootDirectory=
diff --git a/test/test-functions b/test/test-functions
index a69fb2cf98..e7656af283 100644
--- a/test/test-functions
+++ b/test/test-functions
@@ -672,6 +672,7 @@ setup_basic_environment() {
has_user_dbus_socket && install_user_dbus
setup_selinux
strip_binaries
+ instmods veth
install_depmod_files
generate_module_dependencies
if get_bool "$IS_BUILT_WITH_ASAN"; then
diff --git a/test/units/testsuite-62-1.service b/test/units/testsuite-62-1.service
new file mode 100644
index 0000000000..b8e15c9925
--- /dev/null
+++ b/test/units/testsuite-62-1.service
@@ -0,0 +1,8 @@
+[Unit]
+Description=TEST-62-RESTRICT-IFACES-all-pings-work
+[Service]
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.1'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.9'
+RestrictNetworkInterfaces=
+Type=oneshot
diff --git a/test/units/testsuite-62-2.service b/test/units/testsuite-62-2.service
new file mode 100644
index 0000000000..51328b0bce
--- /dev/null
+++ b/test/units/testsuite-62-2.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=TEST-62-RESTRICT-IFACES-allow-list
+[Service]
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.1'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5'
+ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.9'
+RestrictNetworkInterfaces=veth0
+RestrictNetworkInterfaces=veth1
+Type=oneshot
diff --git a/test/units/testsuite-62-3.service b/test/units/testsuite-62-3.service
new file mode 100644
index 0000000000..54ab1965ff
--- /dev/null
+++ b/test/units/testsuite-62-3.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=TEST-62-RESTRICT-IFACES-deny-list
+[Service]
+ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.1'
+ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.5'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.9'
+RestrictNetworkInterfaces=~veth0
+RestrictNetworkInterfaces=~veth1
+Type=oneshot
diff --git a/test/units/testsuite-62-4.service b/test/units/testsuite-62-4.service
new file mode 100644
index 0000000000..1d267a9cbe
--- /dev/null
+++ b/test/units/testsuite-62-4.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=TEST-62-RESTRICT-IFACES-empty-assigment
+[Service]
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.1'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.9'
+RestrictNetworkInterfaces=veth0
+RestrictNetworkInterfaces=
+Type=oneshot
diff --git a/test/units/testsuite-62-5.service b/test/units/testsuite-62-5.service
new file mode 100644
index 0000000000..b69485edac
--- /dev/null
+++ b/test/units/testsuite-62-5.service
@@ -0,0 +1,10 @@
+[Unit]
+Description=TEST-62-RESTRICT-IFACES-invert-assigment
+[Service]
+ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.1'
+ExecStart=/bin/sh -c 'ping -c 1 -W 0.2 192.168.113.5'
+ExecStart=/bin/sh -c '! ping -c 1 -W 0.2 192.168.113.9'
+RestrictNetworkInterfaces=veth0
+RestrictNetworkInterfaces=veth0 veth1
+RestrictNetworkInterfaces=~veth0
+Type=oneshot
diff --git a/test/units/testsuite-62.service b/test/units/testsuite-62.service
new file mode 100644
index 0000000000..faaa2c85e3
--- /dev/null
+++ b/test/units/testsuite-62.service
@@ -0,0 +1,6 @@
+Description=TEST-62-RESTRICT-IFACES
+
+[Service]
+ExecStartPre=rm -f /failed /testok
+ExecStart=/usr/lib/systemd/tests/testdata/units/%N.sh
+Type=oneshot
diff --git a/test/units/testsuite-62.sh b/test/units/testsuite-62.sh
new file mode 100755
index 0000000000..9b22d79fd3
--- /dev/null
+++ b/test/units/testsuite-62.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+set -ex
+set -o pipefail
+
+setup() {
+ systemd-analyze log-level debug
+ systemd-analyze log-target console
+
+ for i in `seq 0 3`;
+ do
+ ip netns del ns${i} || true
+ ip link del veth${i} || true
+ ip netns add ns${i}
+ ip link add veth${i} type veth peer name veth${i}_
+ ip link set veth${i}_ netns ns${i}
+ ip -n ns${i} link set dev veth${i}_ up
+ ip -n ns${i} link set dev lo up
+ ip -n ns${i} addr add "192.168.113."$((4*i+1))/30 dev veth${i}_
+ ip link set dev veth${i} up
+ ip addr add "192.168.113."$((4*i+2))/30 dev veth${i}
+ done
+}
+
+teardown() {
+ set +e
+
+ for i in `seq 0 3`;
+ do
+ ip netns del ns${i}
+ ip link del veth${i}
+ done
+
+ systemd-analyze log-level info
+}
+
+KERNEL_VERSION="$(uname -r)"
+KERNEL_MAJOR="${KERNEL_VERSION%%.*}"
+KERNEL_MINOR="${KERNEL_VERSION#$KERNEL_MAJOR.}"
+KERNEL_MINOR="${KERNEL_MINOR%%.*}"
+
+MAJOR_REQUIRED=5
+MINOR_REQUIRED=7
+
+if [[ "$KERNEL_MAJOR" -lt $MAJOR_REQUIRED || ("$KERNEL_MAJOR" -eq $MAJOR_REQUIRED && "$KERNEL_MINOR" -lt $MINOR_REQUIRED) ]]; then
+ echo "kernel is not 5.7+" >>/skipped
+ exit 0
+fi
+
+trap teardown EXIT
+setup
+
+systemctl start --wait testsuite-62-1.service
+systemctl start --wait testsuite-62-2.service
+systemctl start --wait testsuite-62-3.service
+systemctl start --wait testsuite-62-4.service
+systemctl start --wait testsuite-62-5.service
+
+echo OK > /testok
+
+exit 0