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