diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 695bcaa0a47..d832e68d71c 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -838,6 +838,22 @@ + + KeepConfiguration= + + Takes a boolean or one of static, dhcp-on-stop, + dhcp. When static, systemd-networkd + will not drop static addresses and routes on starting up process. When set to + dhcp-on-stop, systemd-networkd will not drop addresses + and routes on stopping the daemon. When dhcp, + the addresses and routes provided by a DHCP server will never be dropped even if the DHCP + lease expires. This is contrary to the DHCP specification, but may be the best choice if, + e.g., the root filesystem relies on this connection. The setting dhcp + implies dhcp-on-stop, and yes implies + dhcp and static. Defaults to + dhcp-on-stop. + + @@ -1381,17 +1397,6 @@ system. Defaults to no. - - CriticalConnection= - - When true, the connection will never be torn down - even if the DHCP lease expires. This is contrary to the - DHCP specification, but may be the best choice if, say, - the root filesystem relies on this connection. Defaults to - false. - - - ClientIdentifier= diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index 4ed020ea8e5..88a72e39a98 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -464,7 +464,7 @@ static int dhcp_lease_renew(sd_dhcp_client *client, Link *link) { if (r < 0) return log_link_warning_errno(link, r, "DHCP error: no netmask: %m"); - if (!link->network->dhcp_critical) { + if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { r = sd_dhcp_lease_get_lifetime(link->dhcp_lease, &lifetime); if (r < 0) return log_link_warning_errno(link, r, "DHCP error: no lifetime: %m"); @@ -581,7 +581,7 @@ static int dhcp_lease_acquired(sd_dhcp_client *client, Link *link) { } } - if (!link->network->dhcp_critical) { + if (!FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { r = sd_dhcp_lease_get_lifetime(link->dhcp_lease, &lifetime); if (r < 0) return log_link_warning_errno(link, r, "DHCP error: no lifetime: %m"); @@ -649,7 +649,7 @@ static int dhcp4_handler(sd_dhcp_client *client, int event, void *userdata) { return log_link_warning_errno(link, r, "Could not acquire IPv4 link-local address: %m"); } - if (link->network->dhcp_critical) { + if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it."); return 0; } @@ -669,7 +669,7 @@ static int dhcp4_handler(sd_dhcp_client *client, int event, void *userdata) { case SD_DHCP_CLIENT_EVENT_EXPIRED: case SD_DHCP_CLIENT_EVENT_IP_CHANGE: - if (link->network->dhcp_critical) { + if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) { log_link_notice(link, "DHCPv4 connection considered critical, ignoring request to reconfigure it."); return 0; } diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c index 6613992a091..3301d0b3ce6 100644 --- a/src/network/networkd-link.c +++ b/src/network/networkd-link.c @@ -680,14 +680,15 @@ static void link_enter_unmanaged(Link *link) { link_dirty(link); } -int link_stop_clients(Link *link) { +int link_stop_clients(Link *link, bool may_keep_dhcp) { int r = 0, k; assert(link); assert(link->manager); assert(link->manager->event); - if (link->dhcp_client) { + if (link->dhcp_client && (!may_keep_dhcp || !link->network || + !FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP_ON_STOP))) { k = sd_dhcp_client_stop(link->dhcp_client); if (k < 0) r = log_link_warning_errno(link, k, "Could not stop DHCPv4 client: %m"); @@ -731,7 +732,7 @@ void link_enter_failed(Link *link) { link_set_state(link, LINK_STATE_FAILED); - link_stop_clients(link); + link_stop_clients(link, false); link_dirty(link); } @@ -2485,6 +2486,33 @@ static bool link_is_static_route_configured(Link *link, Route *route) { return false; } +static bool link_address_is_dynamic(Link *link, Address *address) { + Route *route; + Iterator i; + + assert(link); + assert(address); + + if (address->cinfo.ifa_prefered != CACHE_INFO_INFINITY_LIFE_TIME) + return true; + + /* Even when the address is leased from a DHCP server, networkd assign the address + * without lifetime when KeepConfiguration=dhcp. So, let's check that we have + * corresponding routes with RTPROT_DHCP. */ + SET_FOREACH(route, link->routes_foreign, i) { + if (route->protocol != RTPROT_DHCP) + continue; + + if (address->family != route->family) + continue; + + if (in_addr_equal(address->family, &address->in_addr, &route->prefsrc)) + return true; + } + + return false; +} + static int link_drop_foreign_config(Link *link) { Address *address; Route *route; @@ -2496,6 +2524,12 @@ static int link_drop_foreign_config(Link *link) { if (address->family == AF_INET6 && in_addr_is_link_local(AF_INET6, &address->in_addr) == 1) continue; + if (link_address_is_dynamic(link, address)) { + if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) + continue; + } else if (FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_STATIC)) + continue; + if (link_is_static_address_configured(link, address)) { r = address_add(link, address->family, &address->in_addr, address->prefixlen, NULL); if (r < 0) @@ -2512,6 +2546,14 @@ static int link_drop_foreign_config(Link *link) { if (route->protocol == RTPROT_KERNEL) continue; + if (route->protocol == RTPROT_STATIC && + FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_STATIC)) + continue; + + if (route->protocol == RTPROT_DHCP && + FLAGS_SET(link->network->keep_configuration, KEEP_CONFIGURATION_DHCP)) + continue; + if (link_is_static_route_configured(link, route)) { r = route_add(link, route->family, &route->dst, route->dst_prefixlen, route->tos, route->priority, route->table, NULL); if (r < 0) @@ -2578,7 +2620,8 @@ static int link_configure(Link *link) { /* Drop foreign config, but ignore loopback or critical devices. * We do not want to remove loopback address or addresses used for root NFS. */ - if (!(link->flags & IFF_LOOPBACK) && !(link->network->dhcp_critical)) { + if (!(link->flags & IFF_LOOPBACK) && + link->network->keep_configuration != KEEP_CONFIGURATION_YES) { r = link_drop_foreign_config(link); if (r < 0) return r; @@ -3264,7 +3307,7 @@ static int link_carrier_lost(Link *link) { if (link->setting_mtu) return 0; - r = link_stop_clients(link); + r = link_stop_clients(link, false); if (r < 0) { link_enter_failed(link); return r; diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h index d0290f7c7d5..03db91bbd97 100644 --- a/src/network/networkd-link.h +++ b/src/network/networkd-link.h @@ -175,7 +175,7 @@ int dhcp6_configure(Link *link); int dhcp6_request_address(Link *link, int ir); int dhcp6_lease_pd_prefix_lost(sd_dhcp6_client *client, Link* link); -int link_stop_clients(Link *link); +int link_stop_clients(Link *link, bool may_keep_dhcp); const char* link_state_to_string(LinkState s) _const_; LinkState link_state_from_string(const char *s) _pure_; diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c index 6984c5b9679..e1f4d7623f5 100644 --- a/src/network/networkd-manager.c +++ b/src/network/networkd-manager.c @@ -1447,7 +1447,7 @@ void manager_free(Manager *m) { if (link->dhcp6_client) (void) dhcp6_lease_pd_prefix_lost(link->dhcp6_client, link); - link_stop_clients(link); + (void) link_stop_clients(link, true); link_unref(link); } diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 9ef07ea372b..2986ff1123d 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -87,6 +87,7 @@ Network.IPv6ProxyNDPAddress, config_parse_ipv6_proxy_ndp_address, Network.BindCarrier, config_parse_strv, 0, offsetof(Network, bind_carrier) Network.ConfigureWithoutCarrier, config_parse_bool, 0, offsetof(Network, configure_without_carrier) Network.IgnoreCarrierLoss, config_parse_bool, 0, offsetof(Network, ignore_carrier_loss) +Network.KeepConfiguration, config_parse_keep_configuration, 0, offsetof(Network, keep_configuration) Address.Address, config_parse_address, 0, 0 Address.Peer, config_parse_address, 0, 0 Address.Broadcast, config_parse_broadcast, 0, 0 @@ -143,7 +144,7 @@ DHCP.Anonymize, config_parse_bool, DHCP.SendHostname, config_parse_bool, 0, offsetof(Network, dhcp_send_hostname) DHCP.Hostname, config_parse_hostname, 0, offsetof(Network, dhcp_hostname) DHCP.RequestBroadcast, config_parse_bool, 0, offsetof(Network, dhcp_broadcast) -DHCP.CriticalConnection, config_parse_bool, 0, offsetof(Network, dhcp_critical) +DHCP.CriticalConnection, config_parse_tristate, 0, offsetof(Network, dhcp_critical) /* deprecated */ DHCP.VendorClassIdentifier, config_parse_string, 0, offsetof(Network, dhcp_vendor_class_identifier) DHCP.MaxAttempts, config_parse_dhcp_max_attempts, 0, 0 DHCP.UserClass, config_parse_dhcp_user_class, 0, offsetof(Network, dhcp_user_class) diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index a5e7cad58a4..0741dfe646d 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -239,6 +239,22 @@ int network_verify(Network *network) { network->dhcp_use_mtu = false; } + if (network->dhcp_critical >= 0) { + if (network->keep_configuration >= 0) + log_warning("%s: Both KeepConfiguration= and deprecated CriticalConnection= are set. " + "Ignoring CriticalConnection=.", network->filename); + else if (network->dhcp_critical) + /* CriticalConnection=yes also preserve foreign static configurations. */ + network->keep_configuration = KEEP_CONFIGURATION_YES; + else + /* For backward compatibility, we do not release DHCP addresses on manager stop. */ + network->keep_configuration = KEEP_CONFIGURATION_DHCP_ON_STOP; + } + + if (network->keep_configuration < 0) + /* For backward compatibility, we do not release DHCP addresses on manager stop. */ + network->keep_configuration = KEEP_CONFIGURATION_DHCP_ON_STOP; + LIST_FOREACH_SAFE(addresses, address, address_next, network->static_addresses) if (address_section_verify(address) < 0) address_free(address); @@ -324,6 +340,7 @@ int network_load_one(Manager *manager, const char *filename) { .required_for_online = true, .required_operstate_for_online = LINK_OPERSTATE_DEGRADED, .dhcp = ADDRESS_FAMILY_NO, + .dhcp_critical = -1, .dhcp_use_ntp = true, .dhcp_use_dns = true, .dhcp_use_hostname = true, @@ -392,6 +409,8 @@ int network_load_one(Manager *manager, const char *filename) { .ipv6_accept_ra_route_table = RT_TABLE_MAIN, .ipv6_accept_ra_route_table_set = false, + .keep_configuration = _KEEP_CONFIGURATION_INVALID, + .can_triple_sampling = -1, }; @@ -1752,3 +1771,16 @@ int config_parse_required_for_online( return 0; } + +DEFINE_CONFIG_PARSE_ENUM(config_parse_keep_configuration, keep_configuration, KeepConfiguration, + "Failed to parse KeepConfiguration= setting"); + +static const char* const keep_configuration_table[_KEEP_CONFIGURATION_MAX] = { + [KEEP_CONFIGURATION_NO] = "no", + [KEEP_CONFIGURATION_DHCP_ON_STOP] = "dhcp-on-stop", + [KEEP_CONFIGURATION_DHCP] = "dhcp", + [KEEP_CONFIGURATION_STATIC] = "static", + [KEEP_CONFIGURATION_YES] = "yes", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(keep_configuration, KeepConfiguration, KEEP_CONFIGURATION_YES); diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index d2a0b8c5f14..7b92a544266 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -78,6 +78,17 @@ typedef enum RADVPrefixDelegation { _RADV_PREFIX_DELEGATION_INVALID = -1, } RADVPrefixDelegation; +typedef enum KeepConfiguration { + KEEP_CONFIGURATION_NO = 0, + KEEP_CONFIGURATION_DHCP_ON_START = 1 << 0, + KEEP_CONFIGURATION_DHCP_ON_STOP = 1 << 1, + KEEP_CONFIGURATION_DHCP = KEEP_CONFIGURATION_DHCP_ON_START | KEEP_CONFIGURATION_DHCP_ON_STOP, + KEEP_CONFIGURATION_STATIC = 1 << 2, + KEEP_CONFIGURATION_YES = KEEP_CONFIGURATION_DHCP | KEEP_CONFIGURATION_STATIC, + _KEEP_CONFIGURATION_MAX, + _KEEP_CONFIGURATION_INVALID = -1, +} KeepConfiguration; + typedef struct Manager Manager; struct Network { @@ -119,7 +130,7 @@ struct Network { bool dhcp_anonymize; bool dhcp_send_hostname; bool dhcp_broadcast; - bool dhcp_critical; + int dhcp_critical; bool dhcp_use_dns; bool dhcp_use_ntp; bool dhcp_use_mtu; @@ -227,6 +238,7 @@ struct Network { bool unmanaged; bool configure_without_carrier; bool ignore_carrier_loss; + KeepConfiguration keep_configuration; uint32_t iaid; DUID duid; @@ -318,6 +330,7 @@ CONFIG_PARSER_PROTOTYPE(config_parse_ntp); CONFIG_PARSER_PROTOTYPE(config_parse_iaid); CONFIG_PARSER_PROTOTYPE(config_parse_required_for_online); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_max_attempts); +CONFIG_PARSER_PROTOTYPE(config_parse_keep_configuration); /* Legacy IPv4LL support */ CONFIG_PARSER_PROTOTYPE(config_parse_ipv4ll); @@ -336,3 +349,6 @@ DHCPUseDomains dhcp_use_domains_from_string(const char *s) _pure_; const char* radv_prefix_delegation_to_string(RADVPrefixDelegation i) _const_; RADVPrefixDelegation radv_prefix_delegation_from_string(const char *s) _pure_; + +const char* keep_configuration_to_string(KeepConfiguration i) _const_; +KeepConfiguration keep_configuration_from_string(const char *s) _pure_; diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network index bba8948d352..496c52336cb 100644 --- a/test/fuzz/fuzz-network-parser/directives.network +++ b/test/fuzz/fuzz-network-parser/directives.network @@ -135,6 +135,7 @@ DHCPServer= BindCarrier= VRF= IgnoreCarrierLoss= +KeepConfiguration= [IPv6Prefix] Prefix= OnLink= diff --git a/test/test-network/conf/24-keep-configuration-static.network b/test/test-network/conf/24-keep-configuration-static.network new file mode 100644 index 00000000000..0c65a1d9e2e --- /dev/null +++ b/test/test-network/conf/24-keep-configuration-static.network @@ -0,0 +1,5 @@ +[Match] +Name=dummy98 + +[Network] +KeepConfiguration=static diff --git a/test/test-network/conf/dhcp-client-critical-connection.network b/test/test-network/conf/dhcp-client-keep-configuration-dhcp-on-stop.network similarity index 65% rename from test/test-network/conf/dhcp-client-critical-connection.network rename to test/test-network/conf/dhcp-client-keep-configuration-dhcp-on-stop.network index 0e65dec0ae9..e17c9854021 100644 --- a/test/test-network/conf/dhcp-client-critical-connection.network +++ b/test/test-network/conf/dhcp-client-keep-configuration-dhcp-on-stop.network @@ -4,6 +4,4 @@ Name=veth99 [Network] DHCP=ipv4 IPv6AcceptRA=false - -[DHCP] -CriticalConnection=true +KeepConfiguration=dhcp-on-stop diff --git a/test/test-network/conf/dhcp-client-keep-configuration-dhcp.network b/test/test-network/conf/dhcp-client-keep-configuration-dhcp.network new file mode 100644 index 00000000000..c43f78d1dea --- /dev/null +++ b/test/test-network/conf/dhcp-client-keep-configuration-dhcp.network @@ -0,0 +1,7 @@ +[Match] +Name=veth99 + +[Network] +DHCP=ipv4 +IPv6AcceptRA=false +KeepConfiguration=dhcp diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 5ba799036ab..ddb2465e1da 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -1235,6 +1235,7 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): '11-dummy.netdev', '12-dummy.netdev', '23-active-slave.network', + '24-keep-configuration-static.network', '24-search-domain.network', '25-address-link-section.network', '25-address-preferred-lifetime-zero-ipv6.network', @@ -1658,6 +1659,28 @@ class NetworkdNetworkTests(unittest.TestCase, Utilities): self.assertRegex(output, 'DNS: 192.168.42.1') self.assertRegex(output, 'Search Domains: one') + def test_keep_configuration_static(self): + check_output('systemctl stop systemd-networkd') + + check_output('ip link add name dummy98 type dummy') + check_output('ip address add 10.1.2.3/16 dev dummy98') + check_output('ip address add 10.2.3.4/16 dev dummy98 valid_lft 600 preferred_lft 500') + output = check_output('ip address show dummy98') + print(output) + self.assertRegex(output, 'inet 10.1.2.3/16 scope global dummy98') + self.assertRegex(output, 'inet 10.2.3.4/16 scope global dynamic dummy98') + output = check_output('ip route show dev dummy98') + print(output) + + copy_unit_to_networkd_unit_path('24-keep-configuration-static.network') + start_networkd(0) + wait_online(['dummy98:routable']) + + output = check_output('ip address show dummy98') + print(output) + self.assertRegex(output, 'inet 10.1.2.3/16 scope global dummy98') + self.assertNotRegex(output, 'inet 10.2.3.4/16 scope global dynamic dummy98') + class NetworkdBondTests(unittest.TestCase, Utilities): links = [ 'bond199', @@ -2026,13 +2049,14 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): '25-vrf.netdev', '25-vrf.network', 'dhcp-client-anonymize.network', - 'dhcp-client-critical-connection.network', 'dhcp-client-gateway-onlink-implicit.network', 'dhcp-client-ipv4-dhcp-settings.network', 'dhcp-client-ipv4-only-ipv6-disabled.network', 'dhcp-client-ipv4-only.network', 'dhcp-client-ipv6-only.network', 'dhcp-client-ipv6-rapid-commit.network', + 'dhcp-client-keep-configuration-dhcp-on-stop.network', + 'dhcp-client-keep-configuration-dhcp.network', 'dhcp-client-listen-port.network', 'dhcp-client-route-metric.network', 'dhcp-client-route-table.network', @@ -2210,26 +2234,84 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): print(output) self.assertRegex(output, 'metric 24') - def test_dhcp_route_criticalconnection_true(self): - copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp-v4-server-veth-peer.network', 'dhcp-client-critical-connection.network') + def test_dhcp_keep_configuration_dhcp(self): + copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp-v4-server-veth-peer.network', 'dhcp-client-keep-configuration-dhcp.network') start_networkd(0) wait_online(['veth-peer:carrier']) - start_dnsmasq() + start_dnsmasq(lease_time='2m') wait_online(['veth99:routable', 'veth-peer:routable']) + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertRegex(output, r'192.168.5.*') + output = check_output(*networkctl_cmd, 'status', 'veth99', env=env) print(output) - self.assertRegex(output, '192.168.5.*') + self.assertRegex(output, r'192.168.5.*') # Stopping dnsmasq as networkd won't be allowed to renew the DHCP lease. stop_dnsmasq(dnsmasq_pid_file) # Sleep for 120 sec as the dnsmasq minimum lease time can only be set to 120 + print('Wait for the dynamic address to be expired') time.sleep(125) + print('The lease address should be kept after lease expired') + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertRegex(output, r'192.168.5.*') + output = check_output(*networkctl_cmd, 'status', 'veth99', env=env) print(output) - self.assertRegex(output, '192.168.5.*') + self.assertRegex(output, r'192.168.5.*') + + check_output('systemctl stop systemd-networkd') + + print('The lease address should be kept after networkd stopped') + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertRegex(output, r'192.168.5.*') + + output = check_output(*networkctl_cmd, 'status', 'veth99', env=env) + print(output) + self.assertRegex(output, r'192.168.5.*') + + check_output('systemctl start systemd-networkd') + wait_online(['veth-peer:routable']) + + print('Still the lease address should be kept after networkd restarted') + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertRegex(output, r'192.168.5.*') + + output = check_output(*networkctl_cmd, 'status', 'veth99', env=env) + print(output) + self.assertRegex(output, r'192.168.5.*') + + def test_dhcp_keep_configuration_dhcp_on_stop(self): + copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp-v4-server-veth-peer.network', 'dhcp-client-keep-configuration-dhcp-on-stop.network') + start_networkd(0) + wait_online(['veth-peer:carrier']) + start_dnsmasq(lease_time='2m') + wait_online(['veth99:routable', 'veth-peer:routable']) + + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertRegex(output, r'192.168.5.*') + + stop_dnsmasq(dnsmasq_pid_file) + check_output('systemctl stop systemd-networkd') + + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertRegex(output, r'192.168.5.*') + + start_networkd(0) + wait_online(['veth-peer:routable']) + + output = check_output('ip address show dev veth99 scope global') + print(output) + self.assertNotRegex(output, r'192.168.5.*') def test_dhcp_client_reuse_address_as_static(self): copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp-server-veth-peer.network', 'dhcp-client.network')