view usr/src/cmd/cmd-inet/sbin/dhcpagent/renew.c @ 4:1a15d5aaf794

synchronized with onnv_86 (6202) in onnv-gate
author Koji Uno <koji.uno@sun.com>
date Mon, 31 Aug 2009 14:38:03 +0900
parents c9caec207d52
children
line wrap: on
line source

/*
 * 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));

		if (class_id_len != 0) {
			(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);
}