diff --git a/man/systemd.network.xml b/man/systemd.network.xml index 33c559cddb..d939c29f71 100644 --- a/man/systemd.network.xml +++ b/man/systemd.network.xml @@ -2573,6 +2573,19 @@ NFTSet=prefix:netdev:filter:eth_ipv4_prefix + + IPv6OnlyMode= + + When true, the DHCPv4 configuration will be delayed by the timespan provided by the DHCP + server and skip to configure dynamic IPv4 network connectivity if IPv6 connectivity is provided + within the timespan. See RFC 8925. + Defaults to true when IPv6AcceptRA= is enabled or DHCPv6 client is enabled + (i.e., DHCP=yes), and false otherwise. + + + + + FallbackLeaseLifetimeSec= @@ -3604,6 +3617,20 @@ ServerAddress=192.168.0.1/24 + + IPv6OnlyPreferredSec= + + + Takes a timespan. Controls the + RFC 8925 IPv6-Only Preferred option. + Specifies the DHCPv4 option to indicate that a host supports an IPv6-only mode and is willing to + forgo obtaining an IPv4 address if the network provides IPv6 connectivity. Defaults to unset, and + not send the option. The minimum allowed value is 300 seconds. + + + + + SendOption= diff --git a/src/libsystemd-network/dhcp-lease-internal.h b/src/libsystemd-network/dhcp-lease-internal.h index 7c20e52209..54facc9c65 100644 --- a/src/libsystemd-network/dhcp-lease-internal.h +++ b/src/libsystemd-network/dhcp-lease-internal.h @@ -35,6 +35,7 @@ struct sd_dhcp_lease { usec_t t2; usec_t lifetime; triple_timestamp timestamp; + usec_t ipv6_only_preferred_usec; /* each 0 if unset */ be32_t address; diff --git a/src/libsystemd-network/dhcp-protocol.h b/src/libsystemd-network/dhcp-protocol.h index dd330ae839..4bf2296ee3 100644 --- a/src/libsystemd-network/dhcp-protocol.h +++ b/src/libsystemd-network/dhcp-protocol.h @@ -11,6 +11,11 @@ #include "macro.h" #include "sparse-endian.h" +#include "time-util.h" + +/* RFC 8925 - IPv6-Only Preferred Option for DHCPv4 3.4. + * MIN_V6ONLY_WAIT: The lower boundary for V6ONLY_WAIT. Value: 300 seconds */ +#define MIN_V6ONLY_WAIT_USEC (300U * USEC_PER_SEC) struct DHCPMessage { uint8_t op; diff --git a/src/libsystemd-network/dhcp-server-internal.h b/src/libsystemd-network/dhcp-server-internal.h index a2dae1cd4a..ae61cd8479 100644 --- a/src/libsystemd-network/dhcp-server-internal.h +++ b/src/libsystemd-network/dhcp-server-internal.h @@ -83,6 +83,7 @@ struct sd_dhcp_server { usec_t max_lease_time; usec_t default_lease_time; + usec_t ipv6_only_preferred_usec; sd_dhcp_server_callback_t callback; void *callback_userdata; @@ -105,6 +106,8 @@ typedef struct DHCPRequest { usec_t lifetime; const uint8_t *agent_info_option; char *hostname; + const uint8_t *parameter_request_list; + size_t parameter_request_list_len; } DHCPRequest; extern const struct hash_ops dhcp_lease_hash_ops; diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index facb2b2424..7ecd207993 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -116,6 +116,7 @@ struct sd_dhcp_client { sd_event_source *timeout_t1; sd_event_source *timeout_t2; sd_event_source *timeout_expire; + sd_event_source *timeout_ipv6_only_mode; sd_dhcp_client_callback_t callback; void *userdata; sd_dhcp_client_callback_t state_callback; @@ -125,6 +126,7 @@ struct sd_dhcp_client { int ip_service_type; int socket_priority; bool socket_priority_set; + bool ipv6_acquired; }; static const uint8_t default_req_opts[] = { @@ -280,6 +282,12 @@ int sd_dhcp_client_set_request_option(sd_dhcp_client *client, uint8_t option) { return set_ensure_put(&client->req_opts, NULL, UINT8_TO_PTR(option)); } +static int client_request_contains(sd_dhcp_client *client, uint8_t option) { + assert(client); + + return set_contains(client->req_opts, UINT8_TO_PTR(option)); +} + int sd_dhcp_client_set_request_address( sd_dhcp_client *client, const struct in_addr *last_addr) { @@ -778,6 +786,7 @@ static int client_initialize(sd_dhcp_client *client) { (void) event_source_disable(client->timeout_t1); (void) event_source_disable(client->timeout_t2); (void) event_source_disable(client->timeout_expire); + (void) event_source_disable(client->timeout_ipv6_only_mode); client->attempt = 0; @@ -1424,6 +1433,8 @@ static int client_initialize_time_events(sd_dhcp_client *client) { assert(client); assert(client->event); + (void) event_source_disable(client->timeout_ipv6_only_mode); + if (client->start_delay > 0) { assert_se(sd_event_now(client->event, CLOCK_BOOTTIME, &usec) >= 0); usec = usec_add(usec, client->start_delay); @@ -1613,6 +1624,16 @@ static int client_parse_message( return log_dhcp_client_errno(client, SYNTHETIC_ERRNO(ENOMSG), "received lease lacks subnet mask, and a fallback one cannot be generated, ignoring."); + /* RFC 8925 section 3.2 + * If the client did not include the IPv6-Only Preferred option code in the Parameter Request List in + * the DHCPDISCOVER or DHCPREQUEST message, it MUST ignore the IPv6-Only Preferred option in any + * messages received from the server. */ + if (lease->ipv6_only_preferred_usec > 0 && + !client_request_contains(client, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED)) { + log_dhcp_client(client, "Received message with unrequested IPv6-only preferred option, ignoring the option."); + lease->ipv6_only_preferred_usec = 0; + } + *ret = TAKE_PTR(lease); return 0; } @@ -1639,13 +1660,26 @@ static int client_handle_offer(sd_dhcp_client *client, DHCPMessage *message, siz static int client_enter_requesting(sd_dhcp_client *client) { assert(client); + assert(client->lease); + + if (client->lease->ipv6_only_preferred_usec > 0) { + if (client->ipv6_acquired) { + log_dhcp_client(client, + "Received an OFFER with IPv6-only preferred option, and the host already acquired IPv6 connectivity, stopping DHCPv4 client."); + return sd_dhcp_client_stop(client); + } + + log_dhcp_client(client, + "Received an OFFER with IPv6-only preferred option, delaying to send REQUEST with %s.", + FORMAT_TIMESPAN(client->lease->ipv6_only_preferred_usec, USEC_PER_SEC)); + } client_set_state(client, DHCP_STATE_REQUESTING); client->attempt = 0; return event_reset_time_relative(client->event, &client->timeout_resend, CLOCK_BOOTTIME, - 0, 0, + client->lease->ipv6_only_preferred_usec, 0, client_timeout_resend, client, client->event_priority, "dhcp4-resend-timer", /* force_reset = */ true); @@ -1814,7 +1848,7 @@ static int client_set_lease_timeouts(sd_dhcp_client *client) { return 0; } -static int client_enter_bound(sd_dhcp_client *client, int notify_event) { +static int client_enter_bound_now(sd_dhcp_client *client, int notify_event) { int r; assert(client); @@ -1822,9 +1856,6 @@ static int client_enter_bound(sd_dhcp_client *client, int notify_event) { if (IN_SET(client->state, DHCP_STATE_REQUESTING, DHCP_STATE_REBOOTING)) notify_event = SD_DHCP_CLIENT_EVENT_IP_ACQUIRE; - client->start_delay = 0; - (void) event_source_disable(client->timeout_resend); - client_set_state(client, DHCP_STATE_BOUND); client->attempt = 0; @@ -1853,6 +1884,47 @@ static int client_enter_bound(sd_dhcp_client *client, int notify_event) { return 0; } +static int client_timeout_ipv6_only_mode(sd_event_source *s, uint64_t usec, void *userdata) { + sd_dhcp_client *client = ASSERT_PTR(userdata); + DHCP_CLIENT_DONT_DESTROY(client); + int r; + + r = client_enter_bound_now(client, SD_DHCP_CLIENT_EVENT_IP_ACQUIRE); + if (r < 0) + client_stop(client, r); + + return 0; +} + +static int client_enter_bound(sd_dhcp_client *client, int notify_event) { + assert(client); + assert(client->lease); + + client->start_delay = 0; + (void) event_source_disable(client->timeout_resend); + + if (client->state == DHCP_STATE_REBOOTING && client->lease->ipv6_only_preferred_usec > 0) { + if (client->ipv6_acquired) { + log_dhcp_client(client, + "Received an ACK with IPv6-only preferred option, and the host already acquired IPv6 connectivity, stopping DHCPv4 client."); + return sd_dhcp_client_stop(client); + } + + log_dhcp_client(client, + "Received an ACK with IPv6-only preferred option, delaying to enter bound state with %s.", + FORMAT_TIMESPAN(client->lease->ipv6_only_preferred_usec, USEC_PER_SEC)); + + return event_reset_time_relative(client->event, &client->timeout_ipv6_only_mode, + CLOCK_BOOTTIME, + client->lease->ipv6_only_preferred_usec, 0, + client_timeout_ipv6_only_mode, client, + client->event_priority, "dhcp4-ipv6-only-mode", + /* force_reset = */ true); + } + + return client_enter_bound_now(client, notify_event); +} + static int client_handle_message(sd_dhcp_client *client, DHCPMessage *message, int len) { DHCP_CLIENT_DONT_DESTROY(client); int r; @@ -2108,6 +2180,9 @@ int sd_dhcp_client_start(sd_dhcp_client *client) { assert_return(client, -EINVAL); + /* Note, do not reset the flag in client_initialize(), as it is also called on expire. */ + client->ipv6_acquired = false; + r = client_initialize(client); if (r < 0) return r; @@ -2224,6 +2299,20 @@ int sd_dhcp_client_stop(sd_dhcp_client *client) { return 0; } +int sd_dhcp_client_set_ipv6_connectivity(sd_dhcp_client *client, int have) { + if (!client) + return 0; + + /* We have already received a message with IPv6-Only preferred option, and are waiting for IPv6 + * connectivity or timeout, let's stop the client. */ + if (have && sd_event_source_get_enabled(client->timeout_ipv6_only_mode, NULL) > 0) + return sd_dhcp_client_stop(client); + + /* Otherwise, save that the host already has IPv6 connectivity. */ + client->ipv6_acquired = have; + return 0; +} + int sd_dhcp_client_attach_event(sd_dhcp_client *client, sd_event *event, int64_t priority) { int r; diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c index db8ac7947a..d82ab938f7 100644 --- a/src/libsystemd-network/sd-dhcp-lease.c +++ b/src/libsystemd-network/sd-dhcp-lease.c @@ -884,6 +884,16 @@ int dhcp_lease_parse_options(uint8_t code, uint8_t len, const void *option, void log_debug_errno(r, "Failed to parse 6rd option, ignoring: %m"); break; + case SD_DHCP_OPTION_IPV6_ONLY_PREFERRED: + r = lease_parse_be32_seconds(option, len, /* max_as_infinity = */ false, &lease->ipv6_only_preferred_usec); + if (r < 0) + log_debug_errno(r, "Failed to parse IPv6 only preferred option, ignoring: %m"); + + else if (lease->ipv6_only_preferred_usec < MIN_V6ONLY_WAIT_USEC && + !network_test_mode_enabled()) + lease->ipv6_only_preferred_usec = MIN_V6ONLY_WAIT_USEC; + break; + case SD_DHCP_OPTION_PRIVATE_BASE ... SD_DHCP_OPTION_PRIVATE_LAST: r = dhcp_lease_insert_private_option(lease, code, option, len); if (r < 0) diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index a917406cec..c69572d5b0 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -330,6 +330,15 @@ int sd_dhcp_server_stop(sd_dhcp_server *server) { return 0; } +static bool dhcp_request_contains(DHCPRequest *req, uint8_t option) { + assert(req); + + if (!req->parameter_request_list) + return false; + + return memchr(req->parameter_request_list, option, req->parameter_request_list_len); +} + static int dhcp_server_send_unicast_raw( sd_dhcp_server *server, uint8_t hlen, @@ -649,6 +658,21 @@ static int server_send_offer_or_ack( return r; } + /* RFC 8925 section 3.3. DHCPv4 Server Behavior + * The server MUST NOT include the IPv6-Only Preferred option in the DHCPOFFER or DHCPACK message if + * the option was not present in the Parameter Request List sent by the client. */ + if (dhcp_request_contains(req, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED) && + server->ipv6_only_preferred_usec > 0) { + be32_t sec = usec_to_be32_sec(server->ipv6_only_preferred_usec); + + r = dhcp_option_append( + &packet->dhcp, req->max_optlen, &offset, 0, + SD_DHCP_OPTION_IPV6_ONLY_PREFERRED, + sizeof(sec), &sec); + if (r < 0) + return r; + } + ORDERED_SET_FOREACH(j, server->extra_options) { r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, j->option, j->length, j->data); @@ -778,6 +802,10 @@ static int parse_request(uint8_t code, uint8_t len, const void *option, void *us return 0; } + break; + case SD_DHCP_OPTION_PARAMETER_REQUEST_LIST: + req->parameter_request_list = option; + req->parameter_request_list_len = len; break; } @@ -1501,6 +1529,19 @@ int sd_dhcp_server_set_default_lease_time(sd_dhcp_server *server, uint64_t t) { return 0; } +int sd_dhcp_server_set_ipv6_only_preferred_usec(sd_dhcp_server *server, uint64_t t) { + assert_return(server, -EINVAL); + + /* When 0 is set, disables the IPv6 only mode. */ + + /* Refuse too short timespan unless test mode is enabled. */ + if (t > 0 && t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled()) + return -EINVAL; + + server->ipv6_only_preferred_usec = t; + return 0; +} + int sd_dhcp_server_set_servers( sd_dhcp_server *server, sd_dhcp_lease_server_type_t what, diff --git a/src/network/networkd-address.c b/src/network/networkd-address.c index e2e448b44f..c1a8cd884a 100644 --- a/src/network/networkd-address.c +++ b/src/network/networkd-address.c @@ -1669,7 +1669,7 @@ int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, uint16_t type; Address *address = NULL; Request *req = NULL; - bool is_new = false; + bool is_new = false, update_dhcp4; int ifindex, r; assert(rtnl); @@ -1778,6 +1778,8 @@ int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, assert_not_reached(); } + update_dhcp4 = tmp->family == AF_INET6; + /* Then, find the managed Address and Request objects corresponding to the received address. */ (void) address_get(link, tmp, &address); (void) address_get_request(link, tmp, &req); @@ -1793,7 +1795,7 @@ int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, if (req) address_enter_removed(req->userdata); - return 0; + goto finalize; } if (!address) { @@ -1879,6 +1881,15 @@ int manager_rtnl_process_address(sd_netlink *rtnl, sd_netlink_message *message, if (r < 0) link_enter_failed(link); +finalize: + if (update_dhcp4) { + r = dhcp4_update_ipv6_connectivity(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to notify IPv6 connectivity to DHCPv4 client: %m"); + link_enter_failed(link); + } + } + return 1; } diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c index dacbb7e129..569398571c 100644 --- a/src/network/networkd-dhcp-server.c +++ b/src/network/networkd-dhcp-server.c @@ -6,8 +6,10 @@ #include "sd-dhcp-server.h" +#include "dhcp-protocol.h" #include "fd-util.h" #include "fileio.h" +#include "network-common.h" #include "networkd-address.h" #include "networkd-dhcp-server-bus.h" #include "networkd-dhcp-server-static-lease.h" @@ -402,6 +404,10 @@ static int dhcp4_server_configure(Link *link) { return log_link_error_errno(link, r, "Failed to set default lease time for DHCPv4 server instance: %m"); } + r = sd_dhcp_server_set_ipv6_only_preferred_usec(link->dhcp_server, link->network->dhcp_server_ipv6_only_preferred_usec); + if (r < 0) + return log_link_error_errno(link, r, "Failed to set IPv6 only preferred time for DHCPv4 server instance: %m"); + r = sd_dhcp_server_set_boot_server_address(link->dhcp_server, &link->network->dhcp_server_boot_server_address); if (r < 0) return log_link_warning_errno(link, r, "Failed to set boot server address for DHCPv4 server instance: %m"); @@ -726,3 +732,45 @@ int config_parse_dhcp_server_address( network->dhcp_server_address_prefixlen = prefixlen; return 0; } + +int config_parse_dhcp_server_ipv6_only_preferred( + 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) { + + usec_t t, *usec = ASSERT_PTR(data); + int r; + + assert(filename); + assert(section); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + *usec = 0; + return 0; + } + + r = parse_sec(rvalue, &t); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse [%s] %s=, ignoring assignment: %s", section, lvalue, rvalue); + return 0; + } + + if (t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled()) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Invalid [%s] %s=, ignoring assignment: %s", section, lvalue, rvalue); + return 0; + } + + *usec = t; + return 0; +} diff --git a/src/network/networkd-dhcp-server.h b/src/network/networkd-dhcp-server.h index 4fd4429deb..960232ade6 100644 --- a/src/network/networkd-dhcp-server.h +++ b/src/network/networkd-dhcp-server.h @@ -14,3 +14,4 @@ int link_request_dhcp_server(Link *link); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_relay_agent_suboption); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_emit); CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_address); +CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_ipv6_only_preferred); diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c index 3e8038ede6..57d40e856e 100644 --- a/src/network/networkd-dhcp4.c +++ b/src/network/networkd-dhcp4.c @@ -1452,6 +1452,16 @@ static bool link_needs_dhcp_broadcast(Link *link) { return r == true; } +static bool link_dhcp4_ipv6_only_mode(Link *link) { + assert(link); + assert(link->network); + + if (link->network->dhcp_ipv6_only_mode >= 0) + return link->network->dhcp_ipv6_only_mode; + + return link_dhcp6_enabled(link) || link_ipv6_accept_ra_enabled(link); +} + static int dhcp4_configure(Link *link) { sd_dhcp_option *send_option; void *request_options; @@ -1560,6 +1570,12 @@ static int dhcp4_configure(Link *link) { return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for 6rd: %m"); } + if (link_dhcp4_ipv6_only_mode(link)) { + r = sd_dhcp_client_set_request_option(link->dhcp_client, SD_DHCP_OPTION_IPV6_ONLY_PREFERRED); + if (r < 0) + return log_link_debug_errno(link, r, "DHCPv4 CLIENT: Failed to set request flag for IPv6-only preferred option: %m"); + } + SET_FOREACH(request_options, link->network->dhcp_request_options) { uint32_t option = PTR_TO_UINT32(request_options); @@ -1668,7 +1684,7 @@ int dhcp4_update_mac(Link *link) { return r; if (restart) { - r = sd_dhcp_client_start(link->dhcp_client); + r = dhcp4_start(link); if (r < 0) return r; } @@ -1676,10 +1692,35 @@ int dhcp4_update_mac(Link *link) { return 0; } -int dhcp4_start(Link *link) { +int dhcp4_update_ipv6_connectivity(Link *link) { + assert(link); + + if (!link->network) + return 0; + + if (!link->network->dhcp_ipv6_only_mode) + return 0; + + if (!link->dhcp_client) + return 0; + + /* If the client is running, set the current connectivity. */ + if (sd_dhcp_client_is_running(link->dhcp_client)) + return sd_dhcp_client_set_ipv6_connectivity(link->dhcp_client, link_has_ipv6_connectivity(link)); + + /* If the client has been already stopped or not started yet, let's check the current connectivity + * and start the client if necessary. */ + if (link_has_ipv6_connectivity(link)) + return 0; + + return dhcp4_start_full(link, /* set_ipv6_connectivity = */ false); +} + +int dhcp4_start_full(Link *link, bool set_ipv6_connectivity) { int r; assert(link); + assert(link->network); if (!link->dhcp_client) return 0; @@ -1694,6 +1735,12 @@ int dhcp4_start(Link *link) { if (r < 0) return r; + if (set_ipv6_connectivity) { + r = dhcp4_update_ipv6_connectivity(link); + if (r < 0) + return r; + } + return 1; } diff --git a/src/network/networkd-dhcp4.h b/src/network/networkd-dhcp4.h index 7446f0a131..d36b058546 100644 --- a/src/network/networkd-dhcp4.h +++ b/src/network/networkd-dhcp4.h @@ -15,7 +15,11 @@ typedef enum DHCPClientIdentifier { void network_adjust_dhcp4(Network *network); int dhcp4_update_mac(Link *link); -int dhcp4_start(Link *link); +int dhcp4_update_ipv6_connectivity(Link *link); +int dhcp4_start_full(Link *link, bool set_ipv6_connectivity); +static inline int dhcp4_start(Link *link) { + return dhcp4_start_full(link, true); +} int dhcp4_lease_lost(Link *link); int dhcp4_check_ready(Link *link); diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c index a7b7200423..555b19d580 100644 --- a/src/network/networkd-link.c +++ b/src/network/networkd-link.c @@ -52,6 +52,7 @@ #include "networkd-nexthop.h" #include "networkd-queue.h" #include "networkd-radv.h" +#include "networkd-route-util.h" #include "networkd-route.h" #include "networkd-routing-policy-rule.h" #include "networkd-setlink.h" @@ -94,6 +95,32 @@ bool link_ipv6_enabled(Link *link) { return false; } +bool link_has_ipv6_connectivity(Link *link) { + LinkAddressState ipv6_address_state; + + assert(link); + + link_get_address_states(link, NULL, &ipv6_address_state, NULL); + + switch (ipv6_address_state) { + case LINK_ADDRESS_STATE_ROUTABLE: + /* If the interface has a routable IPv6 address, then we assume yes. */ + return true; + + case LINK_ADDRESS_STATE_DEGRADED: + /* If the interface has only degraded IPv6 address (mostly, link-local address), then let's check + * there is an IPv6 default gateway. */ + return link_has_default_gateway(link, AF_INET6); + + case LINK_ADDRESS_STATE_OFF: + /* No IPv6 address. */ + return false; + + default: + assert_not_reached(); + } +} + static bool link_is_ready_to_configure_one(Link *link, bool allow_unmanaged) { assert(link); diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h index 8ced02f3c1..8bbbc4ec85 100644 --- a/src/network/networkd-link.h +++ b/src/network/networkd-link.h @@ -236,6 +236,7 @@ static inline bool link_has_carrier(Link *link) { bool link_ipv6_enabled(Link *link); int link_ipv6ll_gained(Link *link); +bool link_has_ipv6_connectivity(Link *link); int link_stop_engines(Link *link, bool may_keep_dhcp); diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf index 42e989e116..02fda41243 100644 --- a/src/network/networkd-network-gperf.gperf +++ b/src/network/networkd-network-gperf.gperf @@ -258,6 +258,7 @@ DHCPv4.InitialCongestionWindow, config_parse_tcp_window, DHCPv4.InitialAdvertisedReceiveWindow, config_parse_tcp_window, 0, offsetof(Network, dhcp_advertised_receive_window) DHCPv4.FallbackLeaseLifetimeSec, config_parse_dhcp_fallback_lease_lifetime, 0, 0 DHCPv4.Use6RD, config_parse_bool, 0, offsetof(Network, dhcp_use_6rd) +DHCPv4.IPv6OnlyMode, config_parse_tristate, 0, offsetof(Network, dhcp_ipv6_only_mode) DHCPv4.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, dhcp_netlabel) DHCPv4.NFTSet, config_parse_nft_set, NFT_SET_PARSE_NETWORK, offsetof(Network, dhcp_nft_set_context) DHCPv6.UseAddress, config_parse_bool, 0, offsetof(Network, dhcp6_use_address) @@ -313,6 +314,7 @@ DHCPServer.RelayAgentCircuitId, config_parse_dhcp_server_relay_agen DHCPServer.RelayAgentRemoteId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_remote_id) DHCPServer.MaxLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_max_lease_time_usec) DHCPServer.DefaultLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_default_lease_time_usec) +DHCPServer.IPv6OnlyPreferredSec, config_parse_dhcp_server_ipv6_only_preferred, 0, offsetof(Network, dhcp_server_ipv6_only_preferred_usec) DHCPServer.EmitDNS, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS].emit) DHCPServer.DNS, config_parse_dhcp_server_emit, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_DNS]) DHCPServer.EmitNTP, config_parse_bool, 0, offsetof(Network, dhcp_server_emit[SD_DHCP_LEASE_NTP].emit) diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c index 94506c975d..3de4ea8dec 100644 --- a/src/network/networkd-network.c +++ b/src/network/networkd-network.c @@ -400,6 +400,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi .dhcp_route_table = RT_TABLE_MAIN, .dhcp_ip_service_type = -1, .dhcp_broadcast = -1, + .dhcp_ipv6_only_mode = -1, .dhcp6_use_address = true, .dhcp6_use_pd_prefix = true, diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h index cc2cf36f5c..1091ce289c 100644 --- a/src/network/networkd-network.h +++ b/src/network/networkd-network.h @@ -139,6 +139,7 @@ struct Network { bool dhcp_anonymize; bool dhcp_send_hostname; int dhcp_broadcast; + int dhcp_ipv6_only_mode; bool dhcp_use_dns; bool dhcp_use_dns_set; bool dhcp_routes_to_dns; @@ -221,6 +222,7 @@ struct Network { struct in_addr dhcp_server_boot_server_address; char *dhcp_server_boot_server_name; char *dhcp_server_boot_filename; + usec_t dhcp_server_ipv6_only_preferred_usec; /* link-local addressing support */ AddressFamily link_local; diff --git a/src/network/networkd-route-util.c b/src/network/networkd-route-util.c index b4620079fc..a204fb9631 100644 --- a/src/network/networkd-route-util.c +++ b/src/network/networkd-route-util.c @@ -47,7 +47,8 @@ static bool route_lifetime_is_valid(const Route *route) { route->lifetime_usec > now(CLOCK_BOOTTIME); } -static Route *link_find_default_gateway(Link *link, int family, Route *gw) { +bool link_find_default_gateway(Link *link, int family, Route **gw) { + bool found = false; Route *route; assert(link); @@ -69,16 +70,24 @@ static Route *link_find_default_gateway(Link *link, int family, Route *gw) { continue; if (!in_addr_is_set(route->gw_family, &route->gw)) continue; - if (gw) { - if (route->gw_weight > gw->gw_weight) + + /* Found a default gateway. */ + if (!gw) + return true; + + /* If we have already found another gw, then let's compare their weight and priority. */ + if (*gw) { + if (route->gw_weight > (*gw)->gw_weight) continue; - if (route->priority >= gw->priority) + if (route->priority >= (*gw)->priority) continue; } - gw = route; + + *gw = route; + found = true; } - return gw; + return found; } int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret) { @@ -98,7 +107,7 @@ int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret) { if (link->state != LINK_STATE_CONFIGURED) continue; - gw = link_find_default_gateway(link, family, gw); + link_find_default_gateway(link, family, &gw); } if (!gw) diff --git a/src/network/networkd-route-util.h b/src/network/networkd-route-util.h index 1b082fc63f..f326888c93 100644 --- a/src/network/networkd-route-util.h +++ b/src/network/networkd-route-util.h @@ -9,9 +9,15 @@ typedef struct Link Link; typedef struct Manager Manager; typedef struct Address Address; +typedef struct Route Route; unsigned routes_max(void); +bool link_find_default_gateway(Link *link, int family, Route **gw); +static inline bool link_has_default_gateway(Link *link, int family) { + return link_find_default_gateway(link, family, NULL); +} + int manager_find_uplink(Manager *m, int family, Link *exclude, Link **ret); bool gateway_is_ready(Link *link, bool onlink, int family, const union in_addr_union *gw); diff --git a/src/network/networkd-route.c b/src/network/networkd-route.c index 023d81b578..7218d799fc 100644 --- a/src/network/networkd-route.c +++ b/src/network/networkd-route.c @@ -1620,6 +1620,7 @@ static int process_route_one( _cleanup_(route_freep) Route *tmp = in; Route *route = NULL; + bool update_dhcp4; int r; assert(manager); @@ -1628,6 +1629,8 @@ static int process_route_one( /* link may be NULL. This consumes 'in'. */ + update_dhcp4 = link && tmp->family == AF_INET6 && tmp->dst_prefixlen == 0; + (void) route_get(manager, link, tmp, &route); switch (type) { @@ -1680,6 +1683,14 @@ static int process_route_one( assert_not_reached(); } + if (update_dhcp4) { + r = dhcp4_update_ipv6_connectivity(link); + if (r < 0) { + log_link_warning_errno(link, r, "Failed to notify IPv6 connectivity to DHCPv4 client: %m"); + link_enter_failed(link); + } + } + return 1; } diff --git a/src/systemd/sd-dhcp-client.h b/src/systemd/sd-dhcp-client.h index 19efd3bd87..372603d43e 100644 --- a/src/systemd/sd-dhcp-client.h +++ b/src/systemd/sd-dhcp-client.h @@ -152,6 +152,7 @@ int sd_dhcp_client_start(sd_dhcp_client *client); int sd_dhcp_client_send_release(sd_dhcp_client *client); int sd_dhcp_client_send_decline(sd_dhcp_client *client); int sd_dhcp_client_send_renew(sd_dhcp_client *client); +int sd_dhcp_client_set_ipv6_connectivity(sd_dhcp_client *client, int have); sd_dhcp_client *sd_dhcp_client_ref(sd_dhcp_client *client); sd_dhcp_client *sd_dhcp_client_unref(sd_dhcp_client *client); diff --git a/src/systemd/sd-dhcp-server.h b/src/systemd/sd-dhcp-server.h index 64d95258fd..1256076b83 100644 --- a/src/systemd/sd-dhcp-server.h +++ b/src/systemd/sd-dhcp-server.h @@ -84,6 +84,7 @@ int sd_dhcp_server_set_static_lease(sd_dhcp_server *server, const struct in_addr int sd_dhcp_server_set_max_lease_time(sd_dhcp_server *server, uint64_t t); int sd_dhcp_server_set_default_lease_time(sd_dhcp_server *server, uint64_t t); +int sd_dhcp_server_set_ipv6_only_preferred_usec(sd_dhcp_server *server, uint64_t t); int sd_dhcp_server_forcerenew(sd_dhcp_server *server); diff --git a/test/test-network/conf/25-dhcp-client-ipv6-only-mode.network b/test/test-network/conf/25-dhcp-client-ipv6-only-mode.network new file mode 100644 index 0000000000..21a6bc7eed --- /dev/null +++ b/test/test-network/conf/25-dhcp-client-ipv6-only-mode.network @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth99 + +[Network] +DHCP=ipv4 +IPv6AcceptRA=no + +[DHCPv4] +IPv6OnlyMode=yes diff --git a/test/test-network/conf/25-dhcp-client-ipv6-only.network b/test/test-network/conf/25-dhcp-client-ipv6-only.network index 017f76f4d5..4aba206cb4 100644 --- a/test/test-network/conf/25-dhcp-client-ipv6-only.network +++ b/test/test-network/conf/25-dhcp-client-ipv6-only.network @@ -3,7 +3,8 @@ Name=veth99 [Network] -DHCP=ipv6 +# DHCPv4 is also enabled here, but will be stopped when an IPv6 address is acquired. +DHCP=yes IPv6Token=::1a:2b:3c:4d [Route] diff --git a/test/test-network/conf/25-dhcp-server-ipv6-only-mode.network b/test/test-network/conf/25-dhcp-server-ipv6-only-mode.network new file mode 100644 index 0000000000..cb19e74a69 --- /dev/null +++ b/test/test-network/conf/25-dhcp-server-ipv6-only-mode.network @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=veth-peer + +[Network] +IPv6AcceptRA=false +DHCPServer=yes + +[DHCPServer] +ServerAddress=192.168.5.1/24 +PoolOffset=10 +PoolSize=50 +Router=192.168.5.3 +DNS=_server_address 192.168.5.10 +NTP=_server_address 192.168.5.11 +IPv6OnlyPreferredSec=20s diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 1496a615fe..f0b248917c 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -770,7 +770,11 @@ def setUpModule(): save_timezone() create_service_dropin('systemd-networkd', networkd_bin, - ['[Service]', 'Restart=no', '[Unit]', 'StartLimitIntervalSec=0']) + ['[Service]', + 'Restart=no', + 'Environment=SYSTEMD_NETWORK_TEST_MODE=yes', + '[Unit]', + 'StartLimitIntervalSec=0']) create_service_dropin('systemd-resolved', resolved_bin) create_service_dropin('systemd-timesyncd', timesyncd_bin) @@ -5110,7 +5114,10 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): self.wait_online(['veth-peer:carrier']) # information request mode - start_dnsmasq('--dhcp-option=option6:dns-server,[2600::ee]', + # The name ipv6-only option may not be supported by older dnsmasq + # start_dnsmasq('--dhcp-option=option:ipv6-only,300') + start_dnsmasq('--dhcp-option=108,00:00:02:00', + '--dhcp-option=option6:dns-server,[2600::ee]', '--dhcp-option=option6:ntp-server,[2600::ff]', ra_mode='ra-stateless') self.wait_online(['veth99:routable', 'veth-peer:routable']) @@ -5140,7 +5147,8 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): # solicit mode stop_dnsmasq() - start_dnsmasq('--dhcp-option=option6:dns-server,[2600::ee]', + start_dnsmasq('--dhcp-option=108,00:00:02:00', + '--dhcp-option=option6:dns-server,[2600::ee]', '--dhcp-option=option6:ntp-server,[2600::ff]') networkctl_reconfigure('veth99') self.wait_online(['veth99:routable', 'veth-peer:routable']) @@ -5189,7 +5197,8 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): f.write('\n[DHCPv6]\nRapidCommit=no\n') stop_dnsmasq() - start_dnsmasq('--dhcp-option=option6:dns-server,[2600::ee]', + start_dnsmasq('--dhcp-option=108,00:00:02:00', + '--dhcp-option=option6:dns-server,[2600::ee]', '--dhcp-option=option6:ntp-server,[2600::ff]') networkctl_reload() @@ -5238,16 +5247,30 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): # Note that at this point the DHCPv6 client has not been started because no RA (with managed # bit set) has yet been received and the configuration does not include WithoutRA=true state = get_dhcp6_client_state('veth99') - print(f"State = {state}") + print(f"DHCPv6 client state = {state}") self.assertEqual(state, 'stopped') - start_dnsmasq() + state = get_dhcp4_client_state('veth99') + print(f"DHCPv4 client state = {state}") + self.assertEqual(state, 'selecting') + + start_dnsmasq('--dhcp-option=108,00:00:02:00') self.wait_online(['veth99:routable', 'veth-peer:routable']) state = get_dhcp6_client_state('veth99') - print(f"State = {state}") + print(f"DHCPv6 client state = {state}") self.assertEqual(state, 'bound') + # DHCPv4 client will stop after an DHCPOFFER message received, so we need to wait for a while. + for _ in range(100): + state = get_dhcp4_client_state('veth99') + if state == 'stopped': + break + time.sleep(.2) + + print(f"DHCPv4 client state = {state}") + self.assertEqual(state, 'stopped') + def test_dhcp_client_ipv6_only_with_custom_client_identifier(self): copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client-ipv6-only-custom-client-identifier.network') @@ -5493,6 +5516,18 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): print(f"State = {state}") self.assertEqual(state, 'bound') + def test_dhcp_client_ipv6_only_mode_without_ipv6_connectivity(self): + copy_network_unit('25-veth.netdev', + '25-dhcp-server-ipv6-only-mode.network', + '25-dhcp-client-ipv6-only-mode.network') + start_networkd() + self.wait_online(['veth99:routable', 'veth-peer:routable'], timeout='40s') + self.wait_address('veth99', r'inet 192.168.5.[0-9]*/24', ipv='-4') + + state = get_dhcp4_client_state('veth99') + print(f"State = {state}") + self.assertEqual(state, 'bound') + def test_dhcp_client_ipv4_use_routes_gateway(self): first = True for (routes, gateway, dns_and_ntp_routes, classless) in itertools.product([True, False], repeat=4):