diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index 0d9f1dce0dc..6d7d7e94a50 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -249,6 +249,36 @@
if RequiredForOnline=no.
+
+ ActivationPolicy=
+
+ Specifies the policy for systemd-networkd managing the link
+ administrative state. Specifically, this controls how systemd-networkd
+ changes the network device's IFF_UP flag, which is sometimes
+ controlled by system administrators by running e.g., ip set dev eth0 up
+ or ip set dev eth0 down, and can also be changed with
+ networkctl up eth0 or networkctl down eth0.
+
+ Takes one of up, always-up,
+ manual, always-down, down,
+ or bound. When manual, systemd-networkd
+ will not change the link's admin state automatically; the system administrator must bring the
+ interface up or down manually, as desired. When up (the default) or
+ always-up, or down or always-down,
+ systemd-networkd will set the link up or down, respectively,
+ when the interface is (re)configured. When always-up or
+ always-down, systemd-networkd will set the link up
+ or down, respectively, any time systemd-networkd detects a change in
+ the administrative state. When BindCarrier= is also set, this is
+ automatically set to bound and any other value is ignored.
+
+ The administrative state is not the same as the carrier state, so using
+ always-up does not mean the link will never lose carrier. The link
+ carrier depends on both the administrative state as well as the network device's physical
+ connection. However, to avoid reconfiguration failures, when using always-up,
+ IgnoreCarrierLoss= is forced to true.
+
+
@@ -592,8 +622,9 @@ IPv6Token=prefixstable:2002:da8:1::
A link name or a list of link names. When set, controls the behavior of the current
link. When all links in the list are in an operational down state, the current link is brought
- down. When at least one link has carrier, the current interface is brought up.
-
+ down. When at least one link has carrier, the current interface is brought up.
+
+ This forces ActivationPolicy= to be set to bound.
@@ -966,6 +997,10 @@ IPv6Token=prefixstable:2002:da8:1::
of the interface even if its carrier is lost. When unset, the value specified with
is used.
+
+ When ActivationPolicy= is set to always-up, this
+ is forced to true.
+
diff --git a/src/libsystemd/sd-network/sd-network.c b/src/libsystemd/sd-network/sd-network.c
index b9b1099399f..060d84ae966 100644
--- a/src/libsystemd/sd-network/sd-network.c
+++ b/src/libsystemd/sd-network/sd-network.c
@@ -212,6 +212,27 @@ _public_ int sd_network_link_get_required_operstate_for_online(int ifindex, char
return 0;
}
+_public_ int sd_network_link_get_activation_policy(int ifindex, char **policy) {
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ assert_return(policy, -EINVAL);
+
+ r = network_link_get_string(ifindex, "ACTIVATION_POLICY", &s);
+ if (r < 0) {
+ if (r != -ENODATA)
+ return r;
+
+ /* For compatibility, assuming up. */
+ s = strdup("up");
+ if (!s)
+ return -ENOMEM;
+ }
+
+ *policy = TAKE_PTR(s);
+ return 0;
+}
+
_public_ int sd_network_link_get_llmnr(int ifindex, char **llmnr) {
return network_link_get_string(ifindex, "LLMNR", llmnr);
}
diff --git a/src/network/networkctl.c b/src/network/networkctl.c
index 512f9ba63ba..a37618c8e0b 100644
--- a/src/network/networkctl.c
+++ b/src/network/networkctl.c
@@ -1387,7 +1387,7 @@ static int link_status_one(
_cleanup_strv_free_ char **dns = NULL, **ntp = NULL, **sip = NULL, **search_domains = NULL, **route_domains = NULL;
_cleanup_free_ char *t = NULL, *network = NULL, *iaid = NULL, *duid = NULL,
- *setup_state = NULL, *operational_state = NULL, *lease_file = NULL;
+ *setup_state = NULL, *operational_state = NULL, *lease_file = NULL, *activation_policy = NULL;
const char *driver = NULL, *path = NULL, *vendor = NULL, *model = NULL, *link = NULL,
*on_color_operational, *off_color_operational, *on_color_setup, *off_color_setup;
_cleanup_free_ int *carrier_bound_to = NULL, *carrier_bound_by = NULL;
@@ -2062,6 +2062,16 @@ static int link_status_one(
if (r < 0)
return r;
+ r = sd_network_link_get_activation_policy(info->ifindex, &activation_policy);
+ if (r >= 0) {
+ r = table_add_many(table,
+ TABLE_EMPTY,
+ TABLE_STRING, "Activation Policy:",
+ TABLE_STRING, activation_policy);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
if (lease) {
const void *client_id;
size_t client_id_len;
diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
index 3fad8f12f82..204e0c57ff6 100644
--- a/src/network/networkd-link.c
+++ b/src/network/networkd-link.c
@@ -1831,17 +1831,38 @@ static int link_joined(Link *link) {
assert(link);
assert(link->network);
- if (!hashmap_isempty(link->bound_to_links)) {
+ switch (link->network->activation_policy) {
+ case ACTIVATION_POLICY_BOUND:
r = link_handle_bound_to_list(link);
if (r < 0)
return r;
- } else if (!(link->flags & IFF_UP)) {
+ break;
+ case ACTIVATION_POLICY_UP:
+ if (link->activated)
+ break;
+ _fallthrough_;
+ case ACTIVATION_POLICY_ALWAYS_UP:
r = link_up(link);
if (r < 0) {
link_enter_failed(link);
return r;
}
+ break;
+ case ACTIVATION_POLICY_DOWN:
+ if (link->activated)
+ break;
+ _fallthrough_;
+ case ACTIVATION_POLICY_ALWAYS_DOWN:
+ r = link_down(link, NULL);
+ if (r < 0) {
+ link_enter_failed(link);
+ return r;
+ }
+ break;
+ default:
+ break;
}
+ link->activated = true;
if (link->network->bridge) {
r = link_set_bridge(link);
@@ -2254,6 +2275,7 @@ static int link_reconfigure_internal(Link *link, sd_netlink_message *m, bool for
return r;
link_set_state(link, LINK_STATE_INITIALIZED);
+ link->activated = false;
link_dirty(link);
/* link_configure_duid() returns 0 if it requests product UUID. In that case,
@@ -2658,6 +2680,16 @@ int link_carrier_reset(Link *link) {
static int link_admin_state_up(Link *link) {
int r;
+ assert(link);
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->activation_policy == ACTIVATION_POLICY_ALWAYS_DOWN) {
+ log_link_info(link, "ActivationPolicy is \"always-off\", forcing link down");
+ return link_down(link, NULL);
+ }
+
/* We set the ipv6 mtu after the device mtu, but the kernel resets
* ipv6 mtu on NETDEV_UP, so we need to reset it. The check for
* ipv6_mtu_set prevents this from trying to set it too early before
@@ -2672,6 +2704,21 @@ static int link_admin_state_up(Link *link) {
return 0;
}
+static int link_admin_state_down(Link *link) {
+
+ assert(link);
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->activation_policy == ACTIVATION_POLICY_ALWAYS_UP) {
+ log_link_info(link, "ActivationPolicy is \"always-on\", forcing link up");
+ return link_up(link);
+ }
+
+ return 0;
+}
+
int link_update(Link *link, sd_netlink_message *m) {
_cleanup_strv_free_ char **s = NULL;
hw_addr_data hw_addr;
@@ -2784,9 +2831,14 @@ int link_update(Link *link, sd_netlink_message *m) {
r = link_admin_state_up(link);
if (r < 0)
return r;
- } else if (link_was_admin_up && !(link->flags & IFF_UP))
+ } else if (link_was_admin_up && !(link->flags & IFF_UP)) {
log_link_info(link, "Link DOWN");
+ r = link_admin_state_down(link);
+ if (r < 0)
+ return r;
+ }
+
r = link_update_lldp(link);
if (r < 0)
return r;
@@ -2959,6 +3011,9 @@ int link_save(Link *link) {
st.max != LINK_OPERSTATE_RANGE_DEFAULT.max ? ":" : "",
st.max != LINK_OPERSTATE_RANGE_DEFAULT.max ? strempty(link_operstate_to_string(st.max)) : "");
+ fprintf(f, "ACTIVATION_POLICY=%s\n",
+ activation_policy_to_string(link->network->activation_policy));
+
fprintf(f, "NETWORK_FILE=%s\n", link->network->filename);
/************************************************************/
diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h
index b69e3125f35..3623b1a00dc 100644
--- a/src/network/networkd-link.h
+++ b/src/network/networkd-link.h
@@ -129,6 +129,7 @@ typedef struct Link {
bool setting_genmode:1;
bool ipv6_mtu_set:1;
bool bridge_mdb_configured:1;
+ bool activated:1;
sd_dhcp_server *dhcp_server;
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index 896a8840637..5152868f78a 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -66,6 +66,7 @@ Link.Multicast, config_parse_tristate,
Link.AllMulticast, config_parse_tristate, 0, offsetof(Network, allmulticast)
Link.Promiscuous, config_parse_tristate, 0, offsetof(Network, promiscuous)
Link.Unmanaged, config_parse_bool, 0, offsetof(Network, unmanaged)
+Link.ActivationPolicy, config_parse_activation_policy, 0, offsetof(Network, activation_policy)
Link.RequiredForOnline, config_parse_required_for_online, 0, 0
SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, 0
SR-IOV.VLANId, config_parse_sr_iov_uint32, 0, 0
diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c
index 32f91402379..ad4cd46276d 100644
--- a/src/network/networkd-network.c
+++ b/src/network/networkd-network.c
@@ -225,9 +225,6 @@ int network_verify(Network *network) {
if (network->dhcp_use_gateway < 0)
network->dhcp_use_gateway = network->dhcp_use_routes;
- if (network->ignore_carrier_loss < 0)
- network->ignore_carrier_loss = network->configure_without_carrier;
-
if (network->dhcp_critical >= 0) {
if (network->keep_configuration >= 0)
log_warning("%s: Both KeepConfiguration= and deprecated CriticalConnection= are set. "
@@ -239,6 +236,30 @@ int network_verify(Network *network) {
network->keep_configuration = KEEP_CONFIGURATION_NO;
}
+ if (!strv_isempty(network->bind_carrier)) {
+ if (!IN_SET(network->activation_policy, _ACTIVATION_POLICY_INVALID, ACTIVATION_POLICY_BOUND))
+ log_warning("%s: ActivationPolicy=bound is required with BindCarrier=. "
+ "Setting ActivationPolicy=bound.", network->filename);
+ network->activation_policy = ACTIVATION_POLICY_BOUND;
+ } else if (network->activation_policy == ACTIVATION_POLICY_BOUND) {
+ log_warning("%s: ActivationPolicy=bound requires BindCarrier=. "
+ "Ignoring ActivationPolicy=bound.", network->filename);
+ network->activation_policy = ACTIVATION_POLICY_UP;
+ }
+
+ if (network->activation_policy == _ACTIVATION_POLICY_INVALID)
+ network->activation_policy = ACTIVATION_POLICY_UP;
+
+ if (network->activation_policy == ACTIVATION_POLICY_ALWAYS_UP) {
+ if (network->ignore_carrier_loss == false)
+ log_warning("%s: IgnoreCarrierLoss=false conflicts with ActivationPolicy=always-up. "
+ "Setting IgnoreCarrierLoss=true.", network->filename);
+ network->ignore_carrier_loss = true;
+ }
+
+ if (network->ignore_carrier_loss < 0)
+ network->ignore_carrier_loss = network->configure_without_carrier;
+
if (network->keep_configuration < 0)
network->keep_configuration = KEEP_CONFIGURATION_NO;
@@ -316,6 +337,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi
.required_for_online = true,
.required_operstate_for_online = LINK_OPERSTATE_RANGE_DEFAULT,
+ .activation_policy = _ACTIVATION_POLICY_INVALID,
.arp = -1,
.multicast = -1,
.allmulticast = -1,
@@ -1247,3 +1269,15 @@ static const char* const ipv6_link_local_address_gen_mode_table[_IPV6_LINK_LOCAL
DEFINE_STRING_TABLE_LOOKUP(ipv6_link_local_address_gen_mode, IPv6LinkLocalAddressGenMode);
DEFINE_CONFIG_PARSE_ENUM(config_parse_ipv6_link_local_address_gen_mode, ipv6_link_local_address_gen_mode, IPv6LinkLocalAddressGenMode, "Failed to parse IPv6 link local address generation mode");
+
+static const char* const activation_policy_table[_ACTIVATION_POLICY_MAX] = {
+ [ACTIVATION_POLICY_UP] = "up",
+ [ACTIVATION_POLICY_ALWAYS_UP] = "always-up",
+ [ACTIVATION_POLICY_MANUAL] = "manual",
+ [ACTIVATION_POLICY_ALWAYS_DOWN] = "always-down",
+ [ACTIVATION_POLICY_DOWN] = "down",
+ [ACTIVATION_POLICY_BOUND] = "bound",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(activation_policy, ActivationPolicy);
+DEFINE_CONFIG_PARSE_ENUM(config_parse_activation_policy, activation_policy, ActivationPolicy, "Failed to parse activation policy");
diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h
index bd419f6ef41..52aae6a31e6 100644
--- a/src/network/networkd-network.h
+++ b/src/network/networkd-network.h
@@ -47,6 +47,17 @@ typedef enum IPv6LinkLocalAddressGenMode {
_IPV6_LINK_LOCAL_ADDRESS_GEN_MODE_INVALID = -1
} IPv6LinkLocalAddressGenMode;
+typedef enum ActivationPolicy {
+ ACTIVATION_POLICY_UP,
+ ACTIVATION_POLICY_ALWAYS_UP,
+ ACTIVATION_POLICY_MANUAL,
+ ACTIVATION_POLICY_ALWAYS_DOWN,
+ ACTIVATION_POLICY_DOWN,
+ ACTIVATION_POLICY_BOUND,
+ _ACTIVATION_POLICY_MAX,
+ _ACTIVATION_POLICY_INVALID = -1
+} ActivationPolicy;
+
typedef struct Manager Manager;
typedef struct NetworkDHCPServerEmitAddress {
@@ -93,6 +104,7 @@ struct Network {
bool unmanaged;
bool required_for_online; /* Is this network required to be considered online? */
LinkOperationalStateRange required_operstate_for_online;
+ ActivationPolicy activation_policy;
/* misc settings */
bool configure_without_carrier;
@@ -334,6 +346,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_required_for_online);
CONFIG_PARSER_PROTOTYPE(config_parse_keep_configuration);
CONFIG_PARSER_PROTOTYPE(config_parse_ipv6_link_local_address_gen_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_rx_tx_queues);
+CONFIG_PARSER_PROTOTYPE(config_parse_activation_policy);
const struct ConfigPerfItem* network_network_gperf_lookup(const char *key, GPERF_LEN_TYPE length);
@@ -342,3 +355,6 @@ KeepConfiguration keep_configuration_from_string(const char *s) _pure_;
const char* ipv6_link_local_address_gen_mode_to_string(IPv6LinkLocalAddressGenMode s) _const_;
IPv6LinkLocalAddressGenMode ipv6_link_local_address_gen_mode_from_string(const char *s) _pure_;
+
+const char* activation_policy_to_string(ActivationPolicy i) _const_;
+ActivationPolicy activation_policy_from_string(const char *s) _pure_;
diff --git a/src/systemd/sd-network.h b/src/systemd/sd-network.h
index 7e062514189..884dba81b9b 100644
--- a/src/systemd/sd-network.h
+++ b/src/systemd/sd-network.h
@@ -103,6 +103,11 @@ int sd_network_link_get_address_state(int ifindex, char **state);
*/
int sd_network_link_get_required_for_online(int ifindex);
+/* Get activation policy for ifindex.
+ * Possible values are as specified for ActivationPolicy=
+ */
+int sd_network_link_get_activation_policy(int ifindex, char **policy);
+
/* Get path to .network file applied to link */
int sd_network_link_get_network_file(int ifindex, char **filename);
diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network
index cc91437c164..e6fb9f6a80b 100644
--- a/test/fuzz/fuzz-network-parser/directives.network
+++ b/test/fuzz/fuzz-network-parser/directives.network
@@ -30,6 +30,7 @@ Host=
MACAddress=
PermanentMACAddress=
[Link]
+ActivationPolicy=
RequiredForOnline=
ARP=
AllMulticast=
diff --git a/test/test-network/conf/25-activation-policy.network b/test/test-network/conf/25-activation-policy.network
new file mode 100644
index 00000000000..c53e0919c41
--- /dev/null
+++ b/test/test-network/conf/25-activation-policy.network
@@ -0,0 +1,6 @@
+[Match]
+Name=test1
+
+[Network]
+Address=192.168.10.30/24
+Gateway=192.168.10.1
diff --git a/test/test-network/conf/25-activation-policy.network.d/always-down.conf b/test/test-network/conf/25-activation-policy.network.d/always-down.conf
new file mode 100644
index 00000000000..edfd12e067e
--- /dev/null
+++ b/test/test-network/conf/25-activation-policy.network.d/always-down.conf
@@ -0,0 +1,2 @@
+[Link]
+ActivationPolicy=always-down
diff --git a/test/test-network/conf/25-activation-policy.network.d/always-up.conf b/test/test-network/conf/25-activation-policy.network.d/always-up.conf
new file mode 100644
index 00000000000..b8e0fffedb6
--- /dev/null
+++ b/test/test-network/conf/25-activation-policy.network.d/always-up.conf
@@ -0,0 +1,2 @@
+[Link]
+ActivationPolicy=always-up
diff --git a/test/test-network/conf/25-activation-policy.network.d/down.conf b/test/test-network/conf/25-activation-policy.network.d/down.conf
new file mode 100644
index 00000000000..65af49f73e7
--- /dev/null
+++ b/test/test-network/conf/25-activation-policy.network.d/down.conf
@@ -0,0 +1,2 @@
+[Link]
+ActivationPolicy=down
diff --git a/test/test-network/conf/25-activation-policy.network.d/manual.conf b/test/test-network/conf/25-activation-policy.network.d/manual.conf
new file mode 100644
index 00000000000..8b81ccc5445
--- /dev/null
+++ b/test/test-network/conf/25-activation-policy.network.d/manual.conf
@@ -0,0 +1,2 @@
+[Link]
+ActivationPolicy=manual
diff --git a/test/test-network/conf/25-activation-policy.network.d/up.conf b/test/test-network/conf/25-activation-policy.network.d/up.conf
new file mode 100644
index 00000000000..537380b74c4
--- /dev/null
+++ b/test/test-network/conf/25-activation-policy.network.d/up.conf
@@ -0,0 +1,2 @@
+[Link]
+ActivationPolicy=up
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index b50da26f105..ecd3eaf8f9b 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -1755,6 +1755,7 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities):
'25-address-peer-ipv4.network',
'25-address-preferred-lifetime-zero.network',
'25-address-static.network',
+ '25-activation-policy.network',
'25-bind-carrier.network',
'25-bond-active-backup-slave.netdev',
'25-fibrule-invert.network',
@@ -2711,6 +2712,53 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities):
self.assertRegex(output, 'inet 192.168.10.30/24 brd 192.168.10.255 scope global test1')
self.wait_operstate('test1', 'routable')
+ def _test_activation_policy(self, test):
+ self.setUp()
+ conffile = '25-activation-policy.network'
+ if test:
+ conffile = f'{conffile}.d/{test}.conf'
+ copy_unit_to_networkd_unit_path('11-dummy.netdev', conffile, dropins=False)
+ start_networkd()
+
+ always = test.startswith('always')
+ if test == 'manual':
+ initial_up = 'UP' in check_output('ip link show test1')
+ else:
+ initial_up = not test.endswith('down') # note: default is up
+ expect_up = initial_up
+ next_up = not expect_up
+
+ # if initial expected state is down, must wait for setup_state to reach configuring
+ # so systemd-networkd considers it 'activated'
+ setup_state = None if initial_up else 'configuring'
+
+ for iteration in range(4):
+ with self.subTest(iteration=iteration, expect_up=expect_up):
+ operstate = 'routable' if expect_up else 'off'
+ self.wait_operstate('test1', operstate, setup_state=setup_state, setup_timeout=20)
+ setup_state = None
+
+ if expect_up:
+ self.assertIn('UP', check_output('ip link show test1'))
+ self.assertIn('192.168.10.30/24', check_output('ip address show test1'))
+ self.assertIn('default via 192.168.10.1', check_output('ip route show'))
+ else:
+ self.assertIn('DOWN', check_output('ip link show test1'))
+
+ if next_up:
+ check_output('ip link set dev test1 up')
+ else:
+ check_output('ip link set dev test1 down')
+ expect_up = initial_up if always else next_up
+ next_up = not next_up
+
+ self.tearDown()
+
+ def test_activation_policy(self):
+ for test in ['up', 'always-up', 'manual', 'always-down', 'down', '']:
+ with self.subTest(test=test):
+ self._test_activation_policy(test)
+
def test_domain(self):
copy_unit_to_networkd_unit_path('12-dummy.netdev', '24-search-domain.network')
start_networkd()
@@ -2985,6 +3033,7 @@ class NetworkdStateFileTests(unittest.TestCase, Utilities):
self.assertRegex(data, r'OPER_STATE=routable')
self.assertRegex(data, r'REQUIRED_FOR_ONLINE=yes')
self.assertRegex(data, r'REQUIRED_OPER_STATE_FOR_ONLINE=routable')
+ self.assertRegex(data, r'ACTIVATION_POLICY=up')
self.assertRegex(data, r'NETWORK_FILE=/run/systemd/network/state-file-tests.network')
self.assertRegex(data, r'DNS=10.10.10.10#aaa.com 10.10.10.11:1111#bbb.com \[1111:2222::3333\]:1234#ccc.com')
self.assertRegex(data, r'NTP=0.fedora.pool.ntp.org 1.fedora.pool.ntp.org')