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')