renew.c revision d04ccbb3f3163ae5962a8b7465d9796bff6ca434
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
* or http://www.opensolaris.org/os/licensing.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at usr/src/OPENSOLARIS.LICENSE.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information: Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*/
/*
* Copyright 2007 Sun Microsystems, Inc. All rights reserved.
* Use is subject to license terms.
*/
#pragma ident "%Z%%M% %I% %E% SMI"
#include <sys/types.h>
#include <time.h>
#include <netinet/in.h>
#include <netinet/dhcp.h>
#include <netinet/udp.h>
#include <netinet/ip_var.h>
#include <netinet/udp_var.h>
#include <libinetutil.h>
#include <dhcpmsg.h>
#include <string.h>
#include "packet.h"
#include "agent.h"
#include "script_handler.h"
#include "interface.h"
#include "states.h"
#include "util.h"
/*
* Number of seconds to wait for a retry if the user is interacting with the
* daemon.
*/
#define RETRY_DELAY 10
/*
* If the renew timer fires within this number of seconds of the rebind timer,
* then skip renew. This prevents us from sending back-to-back renew and
* rebind messages -- a pointless activity.
*/
#define TOO_CLOSE 2
static boolean_t stop_extending(dhcp_smach_t *, unsigned int);
/*
* dhcp_renew(): attempts to renew a DHCP lease on expiration of the T1 timer.
*
* input: iu_tq_t *: unused
* void *: the lease to renew (dhcp_lease_t)
* output: void
*
* notes: The primary expense involved with DHCP (like most UDP protocols) is
* with the generation and handling of packets, not the contents of
* those packets. Thus, we try to reduce the number of packets that
* are sent. It would be nice to just renew all leases here (each one
* added has trivial added overhead), but the DHCPv6 RFC doesn't
* explicitly allow that behavior. Rather than having that argument,
* we settle for ones that are close in expiry to the one that fired.
* For v4, we repeatedly reschedule the T1 timer to do the
* retransmissions. For v6, we rely on the common timer computation
* in packet.c.
*/
/* ARGSUSED */
void
dhcp_renew(iu_tq_t *tqp, void *arg)
{
dhcp_lease_t *dlp = arg;
dhcp_smach_t *dsmp = dlp->dl_smach;
uint32_t t2;
dhcpmsg(MSG_VERBOSE, "dhcp_renew: T1 timer expired on %s",
dsmp->dsm_name);
dlp->dl_t1.dt_id = -1;
if (dsmp->dsm_state == RENEWING || dsmp->dsm_state == REBINDING) {
dhcpmsg(MSG_DEBUG, "dhcp_renew: already renewing");
release_lease(dlp);
return;
}
/*
* Sanity check: don't send packets if we're past T2, or if we're
* extremely close.
*/
t2 = dsmp->dsm_curstart_monosec + dlp->dl_t2.dt_start;
if (monosec() + TOO_CLOSE >= t2) {
dhcpmsg(MSG_DEBUG, "dhcp_renew: %spast T2 on %s",
monosec() > t2 ? "" : "almost ", dsmp->dsm_name);
release_lease(dlp);
return;
}
/*
* If there isn't an async event pending, or if we can cancel the one
* that's there, then try to renew by sending an extension request. If
* that fails, we'll try again when the next timer fires.
*/
if (!async_cancel(dsmp) || !async_start(dsmp, DHCP_EXTEND, B_FALSE) ||
!dhcp_extending(dsmp)) {
if (monosec() + RETRY_DELAY < t2) {
/*
* Try again in RETRY_DELAY seconds; user command
* should be gone.
*/
init_timer(&dlp->dl_t1, RETRY_DELAY);
(void) set_smach_state(dsmp, BOUND);
if (!schedule_lease_timer(dlp, &dlp->dl_t1,
dhcp_renew)) {
dhcpmsg(MSG_INFO, "dhcp_renew: unable to "
"reschedule renewal around user command "
"on %s; will wait for rebind",
dsmp->dsm_name);
}
} else {
dhcpmsg(MSG_DEBUG, "dhcp_renew: user busy on %s; will "
"wait for rebind", dsmp->dsm_name);
}
}
release_lease(dlp);
}
/*
* dhcp_rebind(): attempts to renew a DHCP lease from the REBINDING state (T2
* timer expiry).
*
* input: iu_tq_t *: unused
* void *: the lease to renew
* output: void
* notes: For v4, we repeatedly reschedule the T2 timer to do the
* retransmissions. For v6, we rely on the common timer computation
* in packet.c.
*/
/* ARGSUSED */
void
dhcp_rebind(iu_tq_t *tqp, void *arg)
{
dhcp_lease_t *dlp = arg;
dhcp_smach_t *dsmp = dlp->dl_smach;
int nlifs;
dhcp_lif_t *lif;
boolean_t some_valid;
uint32_t expiremax;
DHCPSTATE oldstate;
dhcpmsg(MSG_VERBOSE, "dhcp_rebind: T2 timer expired on %s",
dsmp->dsm_name);
dlp->dl_t2.dt_id = -1;
if ((oldstate = dsmp->dsm_state) == REBINDING) {
dhcpmsg(MSG_DEBUG, "dhcp_renew: already rebinding");
release_lease(dlp);
return;
}
/*
* Sanity check: don't send packets if we've already expired on all of
* the addresses. We compute the maximum expiration time here, because
* it won't matter for v4 (there's only one lease) and for v6 we need
* to know when the last lease ages away.
*/
some_valid = B_FALSE;
expiremax = monosec();
lif = dlp->dl_lifs;
for (nlifs = dlp->dl_nlifs; nlifs > 0; nlifs--, lif = lif->lif_next) {
uint32_t expire;
expire = dsmp->dsm_curstart_monosec + lif->lif_expire.dt_start;
if (expire > expiremax) {
expiremax = expire;
some_valid = B_TRUE;
}
}
if (!some_valid) {
dhcpmsg(MSG_DEBUG, "dhcp_rebind: all leases expired on %s",
dsmp->dsm_name);
release_lease(dlp);
return;
}
/*
* This is our first venture into the REBINDING state, so reset the
* server address. We know the renew timer has already been cancelled
* (or we wouldn't be here).
*/
if (dsmp->dsm_isv6) {
dsmp->dsm_server = ipv6_all_dhcp_relay_and_servers;
} else {
IN6_IPADDR_TO_V4MAPPED(htonl(INADDR_BROADCAST),
&dsmp->dsm_server);
}
/* {Bound,Renew}->rebind transitions cannot fail */
(void) set_smach_state(dsmp, REBINDING);
/*
* If there isn't an async event pending, or if we can cancel the one
* that's there, then try to rebind by sending an extension request.
* If that fails, we'll clean up when the lease expires.
*/
if (!async_cancel(dsmp) || !async_start(dsmp, DHCP_EXTEND, B_FALSE) ||
!dhcp_extending(dsmp)) {
if (monosec() + RETRY_DELAY < expiremax) {
/*
* Try again in RETRY_DELAY seconds; user command
* should be gone.
*/
init_timer(&dlp->dl_t2, RETRY_DELAY);
(void) set_smach_state(dsmp, oldstate);
if (!schedule_lease_timer(dlp, &dlp->dl_t2,
dhcp_rebind)) {
dhcpmsg(MSG_INFO, "dhcp_rebind: unable to "
"reschedule rebind around user command on "
"%s; lease may expire", dsmp->dsm_name);
}
} else {
dhcpmsg(MSG_WARNING, "dhcp_rebind: user busy on %s; "
"will expire", dsmp->dsm_name);
}
}
release_lease(dlp);
}
/*
* dhcp_finish_expire(): finish expiration of a lease after the user script
* runs. If this is the last lease, then restart DHCP.
* The caller has a reference to the LIF, which will be
* dropped.
*
* input: dhcp_smach_t *: the state machine to be restarted
* void *: logical interface that has expired
* output: int: always 1
*/
static int
dhcp_finish_expire(dhcp_smach_t *dsmp, void *arg)
{
dhcp_lif_t *lif = arg;
dhcp_lease_t *dlp;
dhcpmsg(MSG_DEBUG, "lease expired on %s; removing", lif->lif_name);
dlp = lif->lif_lease;
unplumb_lif(lif);
if (dlp->dl_nlifs == 0)
remove_lease(dlp);
release_lif(lif);
/* If some valid leases remain, then drive on */
if (dsmp->dsm_leases != NULL) {
dhcpmsg(MSG_DEBUG,
"dhcp_finish_expire: some leases remain on %s",
dsmp->dsm_name);
return (1);
}
dhcpmsg(MSG_INFO, "last lease expired on %s -- restarting DHCP",
dsmp->dsm_name);
/*
* in the case where the lease is less than DHCP_REBIND_MIN
* seconds, we will never enter dhcp_renew() and thus the packet
* counters will not be reset. in that case, reset them here.
*/
if (dsmp->dsm_state == BOUND) {
dsmp->dsm_bad_offers = 0;
dsmp->dsm_sent = 0;
dsmp->dsm_received = 0;
}
deprecate_leases(dsmp);
/* reset_smach() in dhcp_selecting() will clean up any leftover state */
dhcp_selecting(dsmp);
return (1);
}
/*
* dhcp_deprecate(): deprecates an address on a given logical interface when
* the preferred lifetime expires.
*
* input: iu_tq_t *: unused
* void *: the logical interface whose lease is expiring
* output: void
*/
/* ARGSUSED */
void
dhcp_deprecate(iu_tq_t *tqp, void *arg)
{
dhcp_lif_t *lif = arg;
set_lif_deprecated(lif);
release_lif(lif);
}
/*
* dhcp_expire(): expires a lease on a given logical interface and, if there
* are no more leases, restarts DHCP.
*
* input: iu_tq_t *: unused
* void *: the logical interface whose lease has expired
* output: void
*/
/* ARGSUSED */
void
dhcp_expire(iu_tq_t *tqp, void *arg)
{
dhcp_lif_t *lif = arg;
dhcp_smach_t *dsmp;
const char *event;
dhcpmsg(MSG_VERBOSE, "dhcp_expire: lease timer expired on %s",
lif->lif_name);
lif->lif_expire.dt_id = -1;
if (lif->lif_lease == NULL) {
release_lif(lif);
return;
}
set_lif_deprecated(lif);
dsmp = lif->lif_lease->dl_smach;
if (!async_cancel(dsmp)) {
dhcpmsg(MSG_WARNING,
"dhcp_expire: cannot cancel current asynchronous command "
"on %s", dsmp->dsm_name);
/*
* Try to schedule ourselves for callback. We're really
* situation-critical here; there's not much hope for us if
* this fails.
*/
init_timer(&lif->lif_expire, DHCP_EXPIRE_WAIT);
if (schedule_lif_timer(lif, &lif->lif_expire, dhcp_expire))
return;
dhcpmsg(MSG_CRIT, "dhcp_expire: cannot reschedule dhcp_expire "
"to get called back, proceeding...");
}
if (!async_start(dsmp, DHCP_START, B_FALSE))
dhcpmsg(MSG_WARNING, "dhcp_expire: cannot start asynchronous "
"transaction on %s, continuing...", dsmp->dsm_name);
/*
* Determine if this state machine has any non-expired LIFs left in it.
* If it doesn't, then this is an "expire" event. Otherwise, if some
* valid leases remain, it's a "loss" event. The SOMEEXP case can
* occur only with DHCPv6.
*/
if (expired_lif_state(dsmp) == DHCP_EXP_SOMEEXP)
event = EVENT_LOSS6;
else if (dsmp->dsm_isv6)
event = EVENT_EXPIRE6;
else
event = EVENT_EXPIRE;
/*
* just march on if this fails; at worst someone will be able
* to async_start() while we're actually busy with our own
* asynchronous transaction. better than not having a lease.
*/
(void) script_start(dsmp, event, dhcp_finish_expire, lif, NULL);
}
/*
* dhcp_extending(): sends a REQUEST (IPv4 DHCP) or Rebind/Renew (DHCPv6) to
* extend a lease on a given state machine
*
* input: dhcp_smach_t *: the state machine to send the message from
* output: boolean_t: B_TRUE if the extension request was sent
*/
boolean_t
dhcp_extending(dhcp_smach_t *dsmp)
{
dhcp_pkt_t *dpkt;
stop_pkt_retransmission(dsmp);
/*
* We change state here because this function is also called when
* adopting a lease and on demand by the user.
*/
if (dsmp->dsm_state == BOUND) {
dsmp->dsm_neg_hrtime = gethrtime();
dsmp->dsm_bad_offers = 0;
dsmp->dsm_sent = 0;
dsmp->dsm_received = 0;
/* Bound->renew can't fail */
(void) set_smach_state(dsmp, RENEWING);
}
dhcpmsg(MSG_DEBUG, "dhcp_extending: sending request on %s",
dsmp->dsm_name);
if (dsmp->dsm_isv6) {
dhcp_lease_t *dlp;
dhcp_lif_t *lif;
uint_t nlifs;
uint_t irt, mrt;
/*
* Start constructing the Renew/Rebind message. Only Renew has
* a server ID, as we still think our server might be
* reachable.
*/
if (dsmp->dsm_state == RENEWING) {
dpkt = init_pkt(dsmp, DHCPV6_MSG_RENEW);
(void) add_pkt_opt(dpkt, DHCPV6_OPT_SERVERID,
dsmp->dsm_serverid, dsmp->dsm_serveridlen);
irt = DHCPV6_REN_TIMEOUT;
mrt = DHCPV6_REN_MAX_RT;
} else {
dpkt = init_pkt(dsmp, DHCPV6_MSG_REBIND);
irt = DHCPV6_REB_TIMEOUT;
mrt = DHCPV6_REB_MAX_RT;
}
/*
* Loop over the leases, and add an IA_NA for each and an
* IAADDR for each address.
*/
for (dlp = dsmp->dsm_leases; dlp != NULL; dlp = dlp->dl_next) {
lif = dlp->dl_lifs;
for (nlifs = dlp->dl_nlifs; nlifs > 0;
nlifs--, lif = lif->lif_next) {
(void) add_pkt_lif(dpkt, lif,
DHCPV6_STAT_SUCCESS, NULL);
}
}
/* Add required Option Request option */
(void) add_pkt_prl(dpkt, dsmp);
return (send_pkt_v6(dsmp, dpkt, dsmp->dsm_server,
stop_extending, irt, mrt));
} else {
dhcp_lif_t *lif = dsmp->dsm_lif;
ipaddr_t server;
/* assemble the DHCPREQUEST message. */
dpkt = init_pkt(dsmp, REQUEST);
dpkt->pkt->ciaddr.s_addr = lif->lif_addr;
/*
* The max dhcp message size option is set to the interface
* max, minus the size of the udp and ip headers.
*/
(void) add_pkt_opt16(dpkt, CD_MAX_DHCP_SIZE,
htons(lif->lif_max - sizeof (struct udpiphdr)));
(void) add_pkt_opt32(dpkt, CD_LEASE_TIME, htonl(DHCP_PERM));
(void) add_pkt_opt(dpkt, CD_CLASS_ID, class_id, class_id_len);
(void) add_pkt_prl(dpkt, dsmp);
/*
* dsm_reqhost was set for this state machine in
* dhcp_selecting() if the REQUEST_HOSTNAME option was set and
* a host name was found.
*/
if (dsmp->dsm_reqhost != NULL) {
(void) add_pkt_opt(dpkt, CD_HOSTNAME, dsmp->dsm_reqhost,
strlen(dsmp->dsm_reqhost));
}
(void) add_pkt_opt(dpkt, CD_END, NULL, 0);
IN6_V4MAPPED_TO_IPADDR(&dsmp->dsm_server, server);
return (send_pkt(dsmp, dpkt, server, stop_extending));
}
}
/*
* stop_extending(): decides when to stop retransmitting v4 REQUEST or v6
* Renew/Rebind messages. If we're renewing, then stop if
* T2 is soon approaching.
*
* input: dhcp_smach_t *: the state machine REQUESTs are being sent from
* unsigned int: the number of REQUESTs sent so far
* output: boolean_t: B_TRUE if retransmissions should stop
*/
/* ARGSUSED */
static boolean_t
stop_extending(dhcp_smach_t *dsmp, unsigned int n_requests)
{
dhcp_lease_t *dlp;
/*
* If we're renewing and rebind time is soon approaching, then don't
* schedule
*/
if (dsmp->dsm_state == RENEWING) {
monosec_t t2;
t2 = 0;
for (dlp = dsmp->dsm_leases; dlp != NULL; dlp = dlp->dl_next) {
if (dlp->dl_t2.dt_start > t2)
t2 = dlp->dl_t2.dt_start;
}
t2 += dsmp->dsm_curstart_monosec;
if (monosec() + TOO_CLOSE >= t2) {
dhcpmsg(MSG_DEBUG, "stop_extending: %spast T2 on %s",
monosec() > t2 ? "" : "almost ", dsmp->dsm_name);
return (B_TRUE);
}
}
/*
* Note that returning B_TRUE cancels both this transmission and the
* one that would occur at dsm_send_timeout, and that for v4 we cut the
* time in half for each retransmission. Thus we check here against
* half of the minimum.
*/
if (!dsmp->dsm_isv6 &&
dsmp->dsm_send_timeout < DHCP_REBIND_MIN * MILLISEC / 2) {
dhcpmsg(MSG_DEBUG, "stop_extending: next retry would be in "
"%d.%03d; stopping", dsmp->dsm_send_timeout / MILLISEC,
dsmp->dsm_send_timeout % MILLISEC);
return (B_TRUE);
}
/* Otherwise, w stop only when the next timer (rebind, expire) fires */
return (B_FALSE);
}