networkd: reduce the default IPv4 DAD (ACD) timeout and make it configurable (#37138)

RFC 5227 specifies randomized intervals to avoid that a large number of
hosts powered up at the same time send their message simultaneously.
Performing the conflict detection takes a variable time between 4 and 7
seconds from the beginning to the first announcement, as shown by the
following diagram where P indicates a probe and A an announcement:

```
     time(s)     0   1   2   3   4   5   6   7   8   9
                 +---+---+---+---+---+---+---+---+---+
     SHORTEST    P   P   P       A       A
     LONGEST         P       P       P       A       A
```

The host can't use the address until the first announcement is sent. 7
seconds is a very long time on modern computers especially considering
the fact that the round-trip time on current LAN technologies is at most
few milliseconds. Section 2.2 of the RFC addresses this matter and hints
that a future standard will adjust those timeouts; however that standard
doesn't exist yet.

Make the timeout configurable via a new
"IPv4DuplicateAddressDetectionTimeoutSec=" option. The intervals defined
in the RFC are then scaled proportionally so that the duration of the
conflict detection takes at most the given value. Interval happening
after the first announcement are not scaled, as recommended by the RFC.

Also reduce the default value from 7s to 200ms, which is a more suitable
value for today's technology.
This commit is contained in:
Yu Watanabe
2025-04-26 16:19:33 +09:00
committed by GitHub
10 changed files with 85 additions and 12 deletions

7
NEWS
View File

@@ -109,6 +109,13 @@ CHANGES WITH 258 in spe:
* The meson option 'integration-tests' has been deprecated, and will be
removed in a future release.
systemd-networkd and networkctl:
* systemd-networkd now supports configuring the timeout for IPv4
Duplicate Address Detection via a new setting
IPv4DuplicateAddressDetectionTimeoutSec=. The default timeout value
has been changed from 7 seconds to 200 milliseconds.
— <place>, <date>
CHANGES WITH 257:

View File

@@ -991,6 +991,17 @@ DuplicateAddressDetection=none</programlisting></para>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>IPv4DuplicateAddressDetectionTimeoutSec=</varname></term>
<listitem>
<para>Configures the maximum timeout for IPv4 Duplicate Address Detection (RFC 5227). Must be a
value between 1 millisecond and 60 seconds. If set, Duplicate Address Detection takes a randomized
time between 57% (4/7) and 100% of the given value. If unset, defaults to 200 milliseconds.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>IPv4ReversePathFilter=</varname></term>
<listitem>

View File

@@ -25,18 +25,26 @@
#include "string-util.h"
#include "time-util.h"
/* Constants from the RFC */
#define PROBE_WAIT_USEC (1U * USEC_PER_SEC)
#define PROBE_NUM 3U
#define PROBE_MIN_USEC (1U * USEC_PER_SEC)
#define PROBE_MAX_USEC (2U * USEC_PER_SEC)
#define ANNOUNCE_WAIT_USEC (2U * USEC_PER_SEC)
#define ANNOUNCE_NUM 2U
/* Intervals from the RFC in seconds, need to be multiplied by the time unit */
#define PROBE_WAIT 1U
#define PROBE_MIN 1U
#define PROBE_MAX 2U
#define ANNOUNCE_WAIT 2U
#define TOTAL_TIME_UNITS 7U
/* Intervals from the RFC not adjusted to the time unit */
#define ANNOUNCE_INTERVAL_USEC (2U * USEC_PER_SEC)
#define MAX_CONFLICTS 10U
#define RATE_LIMIT_INTERVAL_USEC (60U * USEC_PER_SEC)
#define DEFEND_INTERVAL_USEC (10U * USEC_PER_SEC)
/* Other constants from the RFC */
#define PROBE_NUM 3U
#define ANNOUNCE_NUM 2U
#define MAX_CONFLICTS 10U
/* Default timeout from the RFC */
#define DEFAULT_ACD_TIMEOUT_USEC (200 * USEC_PER_MSEC)
typedef enum IPv4ACDState {
IPV4ACD_STATE_INIT,
IPV4ACD_STATE_STARTED,
@@ -60,6 +68,10 @@ struct sd_ipv4acd {
unsigned n_iteration;
unsigned n_conflict;
/* Indicates the duration of a "time unit", i.e. one second in the RFC but scaled to the
* chosen total duration. Represents 1/7 of the total conflict detection timeout. */
usec_t time_unit_usec;
sd_event_source *receive_message_event_source;
sd_event_source *timer_event_source;
@@ -150,6 +162,7 @@ int sd_ipv4acd_new(sd_ipv4acd **ret) {
*acd = (sd_ipv4acd) {
.n_ref = 1,
.state = IPV4ACD_STATE_INIT,
.time_unit_usec = DEFAULT_ACD_TIMEOUT_USEC / TOTAL_TIME_UNITS,
.ifindex = -1,
.fd = -EBADF,
};
@@ -218,14 +231,20 @@ static int ipv4acd_on_timeout(sd_event_source *s, uint64_t usec, void *userdata)
case IPV4ACD_STATE_STARTED:
acd->defend_window = 0;
log_ipv4acd(acd,
"Started on address " IPV4_ADDRESS_FMT_STR " with a max timeout of %s",
IPV4_ADDRESS_FMT_VAL(acd->address),
FORMAT_TIMESPAN(TOTAL_TIME_UNITS * acd->time_unit_usec, USEC_PER_MSEC));
ipv4acd_set_state(acd, IPV4ACD_STATE_WAITING_PROBE, true);
if (acd->n_conflict >= MAX_CONFLICTS) {
log_ipv4acd(acd, "Max conflicts reached, delaying by %s",
FORMAT_TIMESPAN(RATE_LIMIT_INTERVAL_USEC, 0));
r = ipv4acd_set_next_wakeup(acd, RATE_LIMIT_INTERVAL_USEC, PROBE_WAIT_USEC);
r = ipv4acd_set_next_wakeup(
acd, RATE_LIMIT_INTERVAL_USEC, PROBE_WAIT * acd->time_unit_usec);
} else
r = ipv4acd_set_next_wakeup(acd, 0, PROBE_WAIT_USEC);
r = ipv4acd_set_next_wakeup(acd, 0, PROBE_WAIT * acd->time_unit_usec);
if (r < 0)
goto fail;
@@ -245,13 +264,16 @@ static int ipv4acd_on_timeout(sd_event_source *s, uint64_t usec, void *userdata)
if (acd->n_iteration < PROBE_NUM - 2) {
ipv4acd_set_state(acd, IPV4ACD_STATE_PROBING, false);
r = ipv4acd_set_next_wakeup(acd, PROBE_MIN_USEC, (PROBE_MAX_USEC-PROBE_MIN_USEC));
r = ipv4acd_set_next_wakeup(
acd,
PROBE_MIN * acd->time_unit_usec,
(PROBE_MAX - PROBE_MIN) * acd->time_unit_usec);
if (r < 0)
goto fail;
} else {
ipv4acd_set_state(acd, IPV4ACD_STATE_WAITING_ANNOUNCE, true);
r = ipv4acd_set_next_wakeup(acd, ANNOUNCE_WAIT_USEC, 0);
r = ipv4acd_set_next_wakeup(acd, ANNOUNCE_WAIT * acd->time_unit_usec, 0);
if (r < 0)
goto fail;
}
@@ -442,6 +464,19 @@ int sd_ipv4acd_set_ifname(sd_ipv4acd *acd, const char *ifname) {
return free_and_strdup(&acd->ifname, ifname);
}
int sd_ipv4acd_set_timeout(sd_ipv4acd *acd, uint64_t usec) {
assert_return(acd, -EINVAL);
if (usec == 0)
usec = DEFAULT_ACD_TIMEOUT_USEC;
/* Clamp the total duration to a value between 1ms and 1 minute */
acd->time_unit_usec = DIV_ROUND_UP(
CLAMP(usec, 1U * USEC_PER_MSEC, 1U * USEC_PER_MINUTE), TOTAL_TIME_UNITS);
return 0;
}
int sd_ipv4acd_get_ifname(sd_ipv4acd *acd, const char **ret) {
int r;

View File

@@ -153,6 +153,12 @@ int sd_ipv4ll_set_mac(sd_ipv4ll *ll, const struct ether_addr *addr) {
return 0;
}
int sd_ipv4ll_set_timeout(sd_ipv4ll *ll, uint64_t usec) {
assert_return(ll, -EINVAL);
return sd_ipv4acd_set_timeout(ll->acd, usec);
}
int sd_ipv4ll_detach_event(sd_ipv4ll *ll) {
assert_return(ll, -EINVAL);

View File

@@ -224,6 +224,7 @@ int ipv4acd_configure(Link *link, const Address *address) {
assert(link);
assert(link->manager);
assert(link->network);
assert(address);
if (address->family != AF_INET)
@@ -268,6 +269,10 @@ int ipv4acd_configure(Link *link, const Address *address) {
if (r < 0)
return r;
r = sd_ipv4acd_set_timeout(acd, link->network->ipv4_dad_timeout_usec);
if (r < 0)
return r;
r = sd_ipv4acd_set_callback(acd, on_acd, link);
if (r < 0)
return r;

View File

@@ -223,6 +223,7 @@ int ipv4ll_configure(Link *link) {
int r;
assert(link);
assert(link->network);
if (!link_ipv4ll_enabled(link))
return 0;
@@ -253,6 +254,10 @@ int ipv4ll_configure(Link *link) {
if (r < 0)
return r;
r = sd_ipv4ll_set_timeout(link->ipv4ll, link->network->ipv4_dad_timeout_usec);
if (r < 0)
return r;
r = sd_ipv4ll_set_ifindex(link->ipv4ll, link->ifindex);
if (r < 0)
return r;

View File

@@ -159,6 +159,7 @@ Network.IPv6PrivacyExtensions, config_parse_ipv6_privacy_extension
Network.IPv6AcceptRA, config_parse_tristate, 0, offsetof(Network, ndisc)
Network.IPv6AcceptRouterAdvertisements, config_parse_tristate, 0, offsetof(Network, ndisc)
Network.IPv6DuplicateAddressDetection, config_parse_int, 0, offsetof(Network, ipv6_dad_transmits)
Network.IPv4DuplicateAddressDetectionTimeoutSec, config_parse_sec, 0, offsetof(Network, ipv4_dad_timeout_usec)
Network.IPv6HopLimit, config_parse_uint8, 0, offsetof(Network, ipv6_hop_limit)
Network.IPv6RetransmissionTimeSec, config_parse_sec, 0, offsetof(Network, ipv6_retransmission_time)
Network.IPv6ProxyNDP, config_parse_tristate, 0, offsetof(Network, ipv6_proxy_ndp)

View File

@@ -112,6 +112,7 @@ struct Network {
char **bind_carrier;
bool default_route_on_device;
AddressFamily ip_masquerade;
usec_t ipv4_dad_timeout_usec;
/* Protocol independent settings */
UseDomains use_domains;

View File

@@ -48,6 +48,7 @@ int sd_ipv4acd_set_ifindex(sd_ipv4acd *acd, int interface_index);
int sd_ipv4acd_get_ifindex(sd_ipv4acd *acd);
int sd_ipv4acd_set_ifname(sd_ipv4acd *acd, const char *interface_name);
int sd_ipv4acd_get_ifname(sd_ipv4acd *acd, const char **ret);
int sd_ipv4acd_set_timeout(sd_ipv4acd *acd, uint64_t usec);
int sd_ipv4acd_set_address(sd_ipv4acd *acd, const struct in_addr *address);
int sd_ipv4acd_is_running(sd_ipv4acd *acd);
int sd_ipv4acd_is_bound(sd_ipv4acd *acd);

View File

@@ -44,6 +44,7 @@ int sd_ipv4ll_get_address(sd_ipv4ll *ll, struct in_addr *address);
int sd_ipv4ll_set_callback(sd_ipv4ll *ll, sd_ipv4ll_callback_t cb, void *userdata);
int sd_ipv4ll_set_check_mac_callback(sd_ipv4ll *ll, sd_ipv4ll_check_mac_callback_t cb, void *userdata);
int sd_ipv4ll_set_mac(sd_ipv4ll *ll, const struct ether_addr *addr);
int sd_ipv4ll_set_timeout(sd_ipv4ll *ll, uint64_t usec);
int sd_ipv4ll_set_ifindex(sd_ipv4ll *ll, int interface_index);
int sd_ipv4ll_get_ifindex(sd_ipv4ll *ll);
int sd_ipv4ll_set_ifname(sd_ipv4ll *ll, const char *interface_name);