diff --git a/man/networkctl.xml b/man/networkctl.xml
index 3a2dc09ecc..1a03e9e11d 100644
--- a/man/networkctl.xml
+++ b/man/networkctl.xml
@@ -461,6 +461,40 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR)
+
+
+
+ mask
+ FILE…
+
+ Mask network configuration files, which include .network,
+ .netdev, and .link files. A symlink of the given name will
+ be created under /etc/ or /run/, depending on
+ whether is specified, that points to /dev/null.
+ If a non-empty config file with the specified name exists under the target directory or a directory
+ with higher priority (e.g. is used while an existing config resides
+ in /etc/), the operation is aborted.
+
+ This command honors in the same way as edit.
+
+
+
+
+
+
+
+ unmask
+ FILE…
+
+ Unmask network configuration files, i.e. reverting the effect of mask.
+ Note that this command operates regardless of the scope of the directory, i.e.
+ is of no effect.
+
+ This command honors in the same way as edit
+ and mask.
+
+
+
@@ -534,11 +568,11 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR)
- When used with edit,
+ When used with edit, mask, or unmask,
systemd-networkd.service8
or
systemd-udevd.service8
- will not be reloaded after the editing finishes.
+ will not be reloaded after the operation finishes.
@@ -547,8 +581,8 @@ s - Service VLAN, m - Two-port MAC Relay (TPMR)
- When used with edit, edit the file under /run/
- instead of /etc/.
+ When used with edit or mask,
+ operate on the file under /run/ instead of /etc/.
diff --git a/src/network/networkctl-config-file.c b/src/network/networkctl-config-file.c
index b212358cfa..670f1c2fd7 100644
--- a/src/network/networkctl-config-file.c
+++ b/src/network/networkctl-config-file.c
@@ -13,6 +13,7 @@
#include "bus-wait-for-jobs.h"
#include "conf-files.h"
#include "edit-util.h"
+#include "mkdir-label.h"
#include "netlink-util.h"
#include "networkctl.h"
#include "networkctl-config-file.h"
@@ -518,3 +519,110 @@ int verb_cat(int argc, char *argv[], void *userdata) {
return ret;
}
+
+int verb_mask(int argc, char *argv[], void *userdata) {
+ ReloadFlags flags = 0;
+ int r;
+
+ r = mac_selinux_init();
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH(name, strv_skip(argv, 1)) {
+ _cleanup_free_ char *config_path = NULL, *symlink_path = NULL;
+ ReloadFlags reload;
+
+ /* We update the real 'flags' at last, since the operation can be skipped. */
+ if (ENDSWITH_SET(*name, ".network", ".netdev"))
+ reload = RELOAD_NETWORKD;
+ else if (endswith(*name, ".link"))
+ reload = RELOAD_UDEVD;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid network config name '%s'.", *name);
+
+ r = get_config_files_by_name(*name, /* allow_masked = */ true, &config_path, /* ret_dropins = */ NULL);
+ if (r == -ENOENT)
+ log_warning("No existing network config '%s' found, proceeding anyway.", *name);
+ else if (r < 0)
+ return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name);
+ else if (!path_startswith(config_path, "/usr")) {
+ r = null_or_empty_path(config_path);
+ if (r < 0)
+ return log_error_errno(r,
+ "Failed to check if '%s' is masked: %m", config_path);
+ if (r > 0) {
+ log_debug("%s is already masked, skipping.", config_path);
+ continue;
+ }
+
+ /* At this point, we have found a config under mutable dir (/run/ or /etc/),
+ * so masking through /run/ (--runtime) is not possible. If it's under /etc/,
+ * then it doesn't work without --runtime either. */
+ if (arg_runtime || path_startswith(config_path, "/etc"))
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
+ "Cannot mask network config %s: %s exists",
+ *name, config_path);
+ }
+
+ symlink_path = path_join(NETWORK_DIRS[arg_runtime ? 1 : 0], *name);
+ if (!symlink_path)
+ return log_oom();
+
+ (void) mkdir_parents_label(symlink_path, 0755);
+
+ if (symlink("/dev/null", symlink_path) < 0)
+ return log_error_errno(errno,
+ "Failed to create symlink '%s' to /dev/null: %m", symlink_path);
+
+ flags |= reload;
+ log_info("Successfully created symlink '%s' to /dev/null.", symlink_path);
+ }
+
+ return reload_daemons(flags);
+}
+
+int verb_unmask(int argc, char *argv[], void *userdata) {
+ ReloadFlags flags = 0;
+ int r;
+
+ STRV_FOREACH(name, strv_skip(argv, 1)) {
+ _cleanup_free_ char *path = NULL;
+ ReloadFlags reload;
+
+ if (ENDSWITH_SET(*name, ".network", ".netdev"))
+ reload = RELOAD_NETWORKD;
+ else if (endswith(*name, ".link"))
+ reload = RELOAD_UDEVD;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid network config name '%s'.", *name);
+
+ r = get_config_files_by_name(*name, /* allow_masked = */ true, &path, /* ret_dropins = */ NULL);
+ if (r == -ENOENT) {
+ log_debug_errno(r, "Network configuration '%s' doesn't exist, skipping.", *name);
+ continue;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name);
+
+ r = null_or_empty_path(path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if '%s' is masked: %m", path);
+ if (r == 0)
+ continue;
+
+ if (path_startswith(path, "/usr"))
+ return log_error_errno(r, "Cannot unmask network config under /usr/: %s", path);
+
+ if (unlink(path) < 0) {
+ if (errno == ENOENT)
+ continue;
+
+ return log_error_errno(errno, "Failed to remove '%s': %m", path);
+ }
+
+ flags |= reload;
+ log_info("Successfully removed masked network config '%s'.", path);
+ }
+
+ return reload_daemons(flags);
+}
diff --git a/src/network/networkctl-config-file.h b/src/network/networkctl-config-file.h
index 8d7069e3f3..38210a8093 100644
--- a/src/network/networkctl-config-file.h
+++ b/src/network/networkctl-config-file.h
@@ -3,3 +3,6 @@
int verb_edit(int argc, char *argv[], void *userdata);
int verb_cat(int argc, char *argv[], void *userdata);
+
+int verb_mask(int argc, char *argv[], void *userdata);
+int verb_unmask(int argc, char *argv[], void *userdata);
diff --git a/src/network/networkctl.c b/src/network/networkctl.c
index aaa2b3a430..cf9d17c8b2 100644
--- a/src/network/networkctl.c
+++ b/src/network/networkctl.c
@@ -2877,6 +2877,8 @@ static int help(void) {
" reload Reload .network and .netdev files\n"
" edit FILES|DEVICES... Edit network configuration files\n"
" cat FILES|DEVICES... Show network configuration files\n"
+ " mask FILES... Mask network configuration files\n"
+ " unmask FILES... Unmask network configuration files\n"
"\nOptions:\n"
" -h --help Show this help\n"
" --version Show package version\n"
@@ -3033,6 +3035,8 @@ static int networkctl_main(int argc, char *argv[]) {
{ "reload", 1, 1, VERB_ONLINE_ONLY, verb_reload },
{ "edit", 2, VERB_ANY, 0, verb_edit },
{ "cat", 2, VERB_ANY, 0, verb_cat },
+ { "mask", 2, VERB_ANY, 0, verb_mask },
+ { "unmask", 2, VERB_ANY, 0, verb_unmask },
{}
};
diff --git a/test/units/testsuite-74.networkctl.sh b/test/units/testsuite-74.networkctl.sh
index b857abcf9a..06a3c39e77 100755
--- a/test/units/testsuite-74.networkctl.sh
+++ b/test/units/testsuite-74.networkctl.sh
@@ -28,6 +28,16 @@ Name=test
EOF
# Test files
+
+networkctl mask --runtime "donotexist.network"
+assert_eq "$(readlink /run/systemd/network/donotexist.network)" "/dev/null"
+networkctl unmask "donotexist.network" # unmask should work even without --runtime
+[[ ! -e /run/systemd/network/donotexist.network ]]
+
+touch /usr/lib/systemd/network/donotexist.network
+(! networkctl unmask "donotexist.network")
+rm /usr/lib/systemd/network/donotexist.network
+
networkctl cat "$NETWORK_NAME" | tail -n +2 | cmp - "/usr/lib/systemd/network/$NETWORK_NAME"
cat >new <"+4" <