diff --git a/src/dhcp.c b/src/dhcp.c index d22a99b1..49bdfc0e 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -1823,15 +1823,23 @@ send_message(struct interface *ifp, uint8_t type, void (*callback)(void *)) state->xid); RT = 0; /* bogus gcc warning */ } else { + unsigned int jitter = ifo->backoff_jitter; + if (state->interval == 0) - state->interval = 4; + state->interval = ifo->initial_interval; else { + unsigned int cutoff = ifo->backoff_cutoff; + state->interval *= 2; - if (state->interval > 64) - state->interval = 64; + if (state->interval > cutoff) + state->interval = cutoff; } - RT = (state->interval * MSEC_PER_SEC) + - (arc4random_uniform(MSEC_PER_SEC * 2) - MSEC_PER_SEC); + + /* Jitter is bounded at config time to the smallest possible + * interval, so the result can never underflow. */ + RT = state->interval * MSEC_PER_SEC + + arc4random_uniform(jitter * 2) - jitter; + /* No carrier? Don't bother sending the packet. * However, we do need to advance the timeout. */ if (!if_is_link_up(ifp)) diff --git a/src/dhcpcd.conf.5.in b/src/dhcpcd.conf.5.in index 0f663aae..6edf8bc0 100644 --- a/src/dhcpcd.conf.5.in +++ b/src/dhcpcd.conf.5.in @@ -303,6 +303,52 @@ You can use this option to stop this from happening. .It Ic fallback Ar profile Fall back to using this profile if DHCP fails. This allows you to configure a static profile instead of using ZeroConf. +.It Ic initial_interval Ar seconds +Set the initial DHCPv4 retransmission interval to +.Ar seconds . +The minimum value is 1 and the maximum is 4. +The default is 4 seconds as per RFC 2131. +This option only affects DHCPv4; +DHCPv6 retransmission is governed by RFC 8415. +See also +.Ic backoff_cutoff +and +.Ic backoff_jitter . +.It Ic backoff_cutoff Ar seconds +Cap the DHCPv4 exponential backoff interval at +.Ar seconds . +The minimum value is 1 and the maximum is 64. +The default is 64 seconds as per RFC 2131. +If +.Ar seconds +is less than +.Ic initial_interval +it will be raised to match it, since the cutoff is a cap on the +exponential growth and must not shrink the initial interval. +Setting both +.Ic initial_interval +and +.Ic backoff_cutoff +to 1 effectively disables exponential growth, so +retransmissions use only the initial interval plus jitter. +This option only affects DHCPv4; +DHCPv6 retransmission is governed by RFC 8415. +See also +.Ic initial_interval +and +.Ic backoff_jitter . +.It Ic backoff_jitter Ar milliseconds +Set the random jitter applied to each DHCPv4 retransmission interval. +The jitter is applied as \(+- +.Ar milliseconds . +The default is 1000 (\(+-1 second) as per RFC 2131, which is also the maximum. +A value of 0 disables jitter, producing deterministic retransmission timing. +This option only affects DHCPv4; +DHCPv6 retransmission is governed by RFC 8415. +See also +.Ic backoff_cutoff +and +.Ic initial_interval . .It Ic fallback_time Ar seconds Start fallback after .Ar seconds . diff --git a/src/if-options.c b/src/if-options.c index d7b9ffb1..0e0f18e2 100644 --- a/src/if-options.c +++ b/src/if-options.c @@ -175,6 +175,9 @@ const struct option cf_options[] = { { "background", no_argument, NULL, 'b' }, { "fallback_time", required_argument, NULL, O_FALLBACK_TIME }, { "ipv4ll_time", required_argument, NULL, O_IPV4LL_TIME }, { "nosyslog", no_argument, NULL, O_NOSYSLOG }, + { "initial_interval", required_argument, NULL, O_INITIAL_INTERVAL }, + { "backoff_cutoff", required_argument, NULL, O_BACKOFF_CUTOFF }, + { "backoff_jitter", required_argument, NULL, O_BACKOFF_JITTER }, { NULL, 0, NULL, '\0' } }; static char * @@ -2552,6 +2555,33 @@ parse_option(struct dhcpcd_ctx *ctx, const char *ifname, struct if_options *ifo, logopts &= ~LOGERR_LOG; logsetopts(logopts); } break; + case O_INITIAL_INTERVAL: + ARG_REQUIRED; + ifo->initial_interval = (uint32_t)strtou(arg, NULL, 0, 1, + MAX_INITIAL_INTERVAL, &e); + if (e) { + logerrx("invalid initial interval: %s", arg); + return -1; + } + break; + case O_BACKOFF_CUTOFF: + ARG_REQUIRED; + ifo->backoff_cutoff = (uint32_t)strtou(arg, NULL, 0, 1, + MAX_BACKOFF_CUTOFF, &e); + if (e) { + logerrx("invalid backoff cutoff: %s", arg); + return -1; + } + break; + case O_BACKOFF_JITTER: + ARG_REQUIRED; + ifo->backoff_jitter = (uint32_t)strtou(arg, NULL, 0, 0, + MAX_BACKOFF_JITTER, &e); + if (e) { + logerrx("invalid backoff jitter: %s", arg); + return -1; + } + break; default: return 0; } @@ -2618,6 +2648,19 @@ finish_config(struct if_options *ifo) if (!(ifo->options & DHCPCD_IPV6RS)) ifo->options &= ~( DHCPCD_IPV6RA_AUTOCONF | DHCPCD_IPV6RA_REQRDNSS); + +#ifdef INET + /* The exponential backoff cutoff must not be lower than the initial + * interval, otherwise the retransmission sequence would shrink rather + * than grow up to the cap. Clamp the cutoff up to the initial + * interval to preserve the documented "cap" semantics. */ + if (ifo->backoff_cutoff < ifo->initial_interval) { + logwarnx("backoff_cutoff (%u) is less than initial_interval " + "(%u); raising backoff_cutoff to match", + ifo->backoff_cutoff, ifo->initial_interval); + ifo->backoff_cutoff = ifo->initial_interval; + } +#endif } static struct if_options * @@ -2637,6 +2680,9 @@ default_config(struct dhcpcd_ctx *ctx) #ifdef INET ifo->fallback_time = DEFAULT_FALLBACK; ifo->ipv4ll_time = DEFAULT_IPV4LL; + ifo->initial_interval = DEFAULT_INITIAL_INTERVAL; + ifo->backoff_cutoff = DEFAULT_BACKOFF_CUTOFF; + ifo->backoff_jitter = DEFAULT_BACKOFF_JITTER; #endif ifo->metric = -1; ifo->auth.options |= DHCPCD_AUTH_REQUIRE; diff --git a/src/if-options.h b/src/if-options.h index bf07eac2..6a2982f3 100644 --- a/src/if-options.h +++ b/src/if-options.h @@ -54,6 +54,15 @@ #define DEFAULT_REQUEST 180 /* secs to request, mirror DHCP6 */ #define DEFAULT_FALLBACK 5 /* secs until fallback */ #define DEFAULT_IPV4LL 5 /* secs until ipv4ll */ +/* DHCPv4 retransmission backoff defaults (RFC 2131); DHCPv4-only. */ +#define DEFAULT_INITIAL_INTERVAL 4 /* DHCP_BASE per RFC 2131 */ +#define DEFAULT_BACKOFF_CUTOFF 64 /* DHCP_MAX per RFC 2131 */ +#define DEFAULT_BACKOFF_JITTER 1000 /* +/- milliseconds */ + +/* Upper bounds to keep the retransmit timeout arithmetic well within range. */ +#define MAX_INITIAL_INTERVAL 4 /* DHCP_BASE per RFC 2131 */ +#define MAX_BACKOFF_CUTOFF 64 /* DHCP_MAX per RFC 2131 */ +#define MAX_BACKOFF_JITTER 1000 /* +/- milliseconds, per RFC 2131 */ #ifndef HOSTNAME_MAX_LEN #define HOSTNAME_MAX_LEN 250 /* 255 - 3 (FQDN) - 2 (DNS enc) */ @@ -191,6 +200,9 @@ #define O_VSIO O_BASE + 57 #define O_VSIO6 O_BASE + 58 #define O_NOSYSLOG O_BASE + 59 +#define O_INITIAL_INTERVAL O_BASE + 60 +#define O_BACKOFF_CUTOFF O_BASE + 61 +#define O_BACKOFF_JITTER O_BASE + 62 extern const struct option cf_options[]; @@ -258,6 +270,9 @@ struct if_options { uint32_t request_time; uint32_t fallback_time; uint32_t ipv4ll_time; + uint32_t initial_interval; + uint32_t backoff_cutoff; + uint32_t backoff_jitter; unsigned long long options; bool randomise_hwaddr;