/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (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 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

#include <string.h>
#include <sys/types.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <dhcpmsg.h>
#include <stddef.h>
#include <assert.h>

#include "states.h"
#include "interface.h"
#include "agent.h"
#include "packet.h"
#include "util.h"

static double	fuzzify(uint32_t, double);
static void 	retransmit(iu_tq_t *, void *);
static uint32_t	next_retransmission(uint32_t);
static int	send_pkt_internal(struct ifslist *);
static uchar_t	pkt_type(PKT *);

/*
 * dhcp_type_ptob(): converts the DHCP packet type values in RFC2131 into
 *		     values which can be used for recv_pkt()
 *
 *   input: uchar_t: a DHCP packet type value, as defined in RFC2131
 *  output: dhcp_message_type_t: a packet type value for use with recv_pkt()
 */

static dhcp_message_type_t
dhcp_type_ptob(uchar_t type)
{
	/*
	 * note: the ordering here allows direct indexing of the table
	 *	 based on the RFC2131 packet type value passed in.
	 */

	static dhcp_message_type_t type_map[] = {
		DHCP_PUNTYPED, DHCP_PDISCOVER, DHCP_POFFER, DHCP_PREQUEST,
		DHCP_PDECLINE, DHCP_PACK, DHCP_PNAK, DHCP_PRELEASE, DHCP_PINFORM
	};

	if (type < (sizeof (type_map) / sizeof (*type_map)))
		return (type_map[type]);

	return (0);
}

/*
 * pkt_type(): returns an integer representing the packet's type; only
 *	       for use with outbound packets.
 *
 *   input: PKT *: the packet to examine
 *  output: uchar_t: the packet type (0 if unknown)
 */

static uchar_t
pkt_type(PKT *pkt)
{
	uchar_t	*option = pkt->options;

	/*
	 * this is a little dirty but it should get the job done.
	 * assumes that the type is in the statically allocated part
	 * of the options field.
	 */

	while (*option != CD_DHCP_TYPE) {
		if (option + 2 - pkt->options >= sizeof (pkt->options))
			return (0);

		option++;
		option += *option;
	}

	return (option[2]);
}

/*
 * init_pkt(): initializes and returns a packet of a given type
 *
 *   input: struct ifslist *: the interface the packet will be going out
 *	    uchar_t: the packet type (DHCP message type)
 *  output: dhcp_pkt_t *: a pointer to the initialized packet
 */

dhcp_pkt_t *
init_pkt(struct ifslist *ifsp, uchar_t type)
{
	uint8_t		bootmagic[] = BOOTMAGIC;
	dhcp_pkt_t	*dpkt = &ifsp->if_send_pkt;
	uint32_t	xid;

	dpkt->pkt_max_len = ifsp->if_max;
	dpkt->pkt_cur_len = offsetof(PKT, options);

	(void) memset(dpkt->pkt, 0, ifsp->if_max);
	(void) memcpy(dpkt->pkt->cookie, bootmagic, sizeof (bootmagic));
	if (ifsp->if_hwlen <= sizeof (dpkt->pkt->chaddr)) {
		dpkt->pkt->hlen  = ifsp->if_hwlen;
		(void) memcpy(dpkt->pkt->chaddr, ifsp->if_hwaddr,
		    ifsp->if_hwlen);
	} else {
		/*
		 * The mac address does not fit in the chaddr
		 * field, thus it can not be sent to the server,
		 * thus server can not unicast the reply. Per
		 * RFC 2131 4.4.1, client can set this bit in
		 * DISCOVER/REQUEST. If the client is already
		 * in BOUND/REBINDING/RENEWING state, do not set
		 * this bit, as it can respond to unicast responses
		 * from server using the 'ciaddr' address.
		 */
		if ((type == DISCOVER) || ((type == REQUEST) &&
		    (ifsp->if_state != RENEWING) &&
		    (ifsp->if_state != REBINDING) &&
		    (ifsp->if_state != BOUND)))
			dpkt->pkt->flags = htons(BCAST_MASK);
	}

	/*
	 * since multiple dhcp leases may be maintained over the same dlpi
	 * device (e.g. "hme0" and "hme0:1"), make sure the xid is unique.
	 */

	do {
		xid = mrand48();
	} while (lookup_ifs_by_xid(xid) != NULL);

	dpkt->pkt->xid	 = xid;
	dpkt->pkt->op    = BOOTREQUEST;
	dpkt->pkt->htype = ifsp->if_hwtype;

	add_pkt_opt(dpkt, CD_DHCP_TYPE, &type, 1);
	add_pkt_opt(dpkt, CD_CLIENT_ID, ifsp->if_cid, ifsp->if_cidlen);

	return (dpkt);
}

/*
 * add_pkt_opt(): adds an option to a dhcp_pkt_t
 *
 *   input: dhcp_pkt_t *: the packet to add the option to
 *	    uchar_t: the type of option being added
 *	    const void *: the value of that option
 *	    uchar_t: the length of the value of the option
 *  output: void
 */

void
add_pkt_opt(dhcp_pkt_t *dpkt, uchar_t opt_type, const void *opt_val,
    uchar_t opt_len)
{
	caddr_t		raw_pkt = (caddr_t)dpkt->pkt;
	int16_t		req_len = opt_len + 2; /* + 2 for code & length bytes */

	/* CD_END and CD_PAD options don't have a length field */
	if (opt_type == CD_END || opt_type == CD_PAD)
		req_len--;
	else if (opt_val == NULL)
		return;

	if ((dpkt->pkt_cur_len + req_len) > dpkt->pkt_max_len) {
		dhcpmsg(MSG_WARNING, "add_pkt_opt: not enough room for option "
		    "%d in packet", opt_type);
		return;
	}

	raw_pkt[dpkt->pkt_cur_len++] = opt_type;

	if (opt_len > 0) {
		raw_pkt[dpkt->pkt_cur_len++] = opt_len;
		(void) memcpy(&raw_pkt[dpkt->pkt_cur_len], opt_val, opt_len);
		dpkt->pkt_cur_len += opt_len;
	}
}

/*
 * add_pkt_opt16(): adds an option with a 16-bit value to a dhcp_pkt_t
 *
 *   input: dhcp_pkt_t *: the packet to add the option to
 *	    uchar_t: the type of option being added
 *	    uint16_t: the value of that option
 *  output: void
 */

void
add_pkt_opt16(dhcp_pkt_t *dpkt, uchar_t opt_type, uint16_t opt_value)
{
	add_pkt_opt(dpkt, opt_type, &opt_value, 2);
}

/*
 * add_pkt_opt32(): adds an option with a 32-bit value to a dhcp_pkt_t
 *
 *   input: dhcp_pkt_t *: the packet to add the option to
 *	    uchar_t: the type of option being added
 *	    uint32_t: the value of that option
 *  output: void
 */

void
add_pkt_opt32(dhcp_pkt_t *dpkt, uchar_t opt_type, uint32_t opt_value)
{
	add_pkt_opt(dpkt, opt_type, &opt_value, 4);
}

/*
 * get_pkt_times(): pulls the lease times out of a packet and stores them as
 *		    host-byteorder relative times in the passed in parameters
 *
 *   input: PKT_LIST *: the packet to pull the packet times from
 *	    lease_t *: where to store the relative lease time in hbo
 *	    lease_t *: where to store the relative t1 time in hbo
 *	    lease_t *: where to store the relative t2 time in hbo
 *  output: void
 */

void
get_pkt_times(PKT_LIST *ack, lease_t *lease, lease_t *t1, lease_t *t2)
{
	*lease	= DHCP_PERM;
	*t1	= DHCP_PERM;
	*t2	= DHCP_PERM;

	if (ack->opts[CD_DHCP_TYPE]  == NULL ||
	    ack->opts[CD_LEASE_TIME] == NULL ||
	    ack->opts[CD_LEASE_TIME]->len != sizeof (lease_t))
		return;

	(void) memcpy(lease, ack->opts[CD_LEASE_TIME]->value, sizeof (lease_t));
	*lease = ntohl(*lease);

	if (*lease == DHCP_PERM)
		return;

	if (ack->opts[CD_T1_TIME] != NULL &&
	    ack->opts[CD_T1_TIME]->len == sizeof (lease_t)) {
		(void) memcpy(t1, ack->opts[CD_T1_TIME]->value, sizeof (*t1));
		*t1 = ntohl(*t1);
	}

	if ((*t1 == DHCP_PERM) || (*t1 >= *lease))
		*t1 = (lease_t)fuzzify(*lease, DHCP_T1_FACT);

	if (ack->opts[CD_T2_TIME] != NULL &&
	    ack->opts[CD_T2_TIME]->len == sizeof (lease_t)) {
		(void) memcpy(t2, ack->opts[CD_T2_TIME]->value, sizeof (*t2));
		*t2 = ntohl(*t2);
	}

	if ((*t2 == DHCP_PERM) || (*t2 > *lease) || (*t2 <= *t1))
		*t2 = (lease_t)fuzzify(*lease, DHCP_T2_FACT);
}

/*
 * fuzzify(): adds some "fuzz" to a t1/t2 time, in accordance with RFC2131
 *
 *   input: uint32_t: the number of seconds until lease expiration
 *	    double: the approximate percentage of that time to return
 *  output: double: a number approximating (sec * pct)
 */

static double
fuzzify(uint32_t sec, double pct)
{
	return (sec * (pct + (drand48() - 0.5) / 25.0));
}

/*
 * free_pkt_list(): frees a packet list
 *
 *   input: PKT_LIST **: the packet list to free
 *  output: void
 */

void
free_pkt_list(PKT_LIST **plp)
{
	PKT_LIST	*plp_next;

	for (; *plp != NULL; *plp = plp_next) {
		plp_next = (*plp)->next;
		free((*plp)->pkt);
		free(*plp);
	}
}

/*
 * prepend_to_pkt_list(): prepends a packet to a packet list
 *
 *   input: PKT_LIST **: the packet list
 *	    PKT_LIST *: the packet to prepend
 *  output: void
 */

static void
prepend_to_pkt_list(PKT_LIST **list_head, PKT_LIST *new_entry)
{
	new_entry->next = *list_head;
	new_entry->prev = NULL;

	if (*list_head != NULL)
		(*list_head)->prev = new_entry;

	*list_head = new_entry;
}

/*
 * remove_from_pkt_list(): removes a given packet from a packet list
 *
 *   input: PKT_LIST **: the packet list
 *	    PKT_LIST *: the packet to remove
 *  output: void
 */

void
remove_from_pkt_list(PKT_LIST **list_head, PKT_LIST *remove)
{
	if (*list_head == NULL)
		return;

	if (*list_head == remove) {
		*list_head = remove->next;
		if (*list_head != NULL)
			(*list_head)->prev = NULL;
	} else {
		remove->prev->next = remove->next;
		if (remove->next != NULL)
			remove->next->prev = remove->prev;
	}

	remove->next = NULL;
	remove->prev = NULL;
}

/*
 * send_pkt_internal(): sends a packet out on an interface
 *
 *   input: struct ifslist *: the interface to send the packet out on
 *  output: int: 1 if the packet is sent, 0 otherwise
 */

static int
send_pkt_internal(struct ifslist *ifsp)
{
	ssize_t		n_bytes;
	dhcp_pkt_t	*dpkt = &ifsp->if_send_pkt;
	const char	*pkt_name = pkt_type_to_string(pkt_type(dpkt->pkt));

	/*
	 * if needed, schedule a retransmission timer, then attempt to
	 * send the packet.  if we fail, then log the error.  our
	 * return value should indicate whether or not we were
	 * successful in sending the request, independent of whether
	 * we could schedule a timer.
	 */

	if (ifsp->if_send_timeout != 0) {
		if ((ifsp->if_retrans_timer = iu_schedule_timer_ms(tq,
		    ifsp->if_send_timeout, retransmit, ifsp)) == -1)
			dhcpmsg(MSG_WARNING, "send_pkt_internal: cannot "
			    "schedule retransmit timer for %s packet",
			    pkt_name);
		else
			hold_ifs(ifsp);
	}

	/*
	 * set the `pkt->secs' field depending on the type of packet.
	 * it should be zero, except in the following cases:
	 *
	 * DISCOVER:	set to the number of seconds since we started
	 *		trying to obtain a lease.
	 *
	 * INFORM:	set to the number of seconds since we started
	 *		trying to get configuration parameters.
	 *
	 * REQUEST:	if in the REQUESTING state, then same value as
	 *		DISCOVER, otherwise the number of seconds
	 *		since we started trying to obtain a lease.
	 *
	 * we also set `if_newstart_monosec', to the time we sent a
	 * REQUEST or DISCOVER packet, so we know the lease start
	 * time (the DISCOVER case is for handling BOOTP servers).
	 */

	switch (pkt_type(dpkt->pkt)) {

	case DISCOVER:
		ifsp->if_newstart_monosec = monosec();
		ifsp->if_disc_secs = monosec() - ifsp->if_neg_monosec;
		dpkt->pkt->secs = htons(ifsp->if_disc_secs);
		break;

	case INFORM:
		dpkt->pkt->secs = htons(monosec() - ifsp->if_neg_monosec);
		break;

	case REQUEST:
		ifsp->if_newstart_monosec = monosec();

		if (ifsp->if_state == REQUESTING) {
			dpkt->pkt->secs = htons(ifsp->if_disc_secs);
			break;
		}

		dpkt->pkt->secs = htons(monosec() - ifsp->if_neg_monosec);
		break;

	default:
		dpkt->pkt->secs = htons(0);
	}

	switch (ifsp->if_state) {

	case BOUND:
	case RENEWING:
	case REBINDING:
		n_bytes = sendto(ifsp->if_sock_ip_fd, dpkt->pkt,
		    dpkt->pkt_cur_len, 0,
		    (struct sockaddr *)&ifsp->if_send_dest,
		    sizeof (struct sockaddr_in));
		break;

	default:
		n_bytes = dlpi_sendto(ifsp->if_dlpi_fd, dpkt->pkt,
		    dpkt->pkt_cur_len, &ifsp->if_send_dest,
		    ifsp->if_daddr, ifsp->if_dlen);
		break;
	}

	if (n_bytes != dpkt->pkt_cur_len) {
		if (ifsp->if_retrans_timer == -1)
			dhcpmsg(MSG_WARNING, "send_pkt_internal: cannot send "
			    "%s packet to server", pkt_name);
		else
			dhcpmsg(MSG_WARNING, "send_pkt_internal: cannot send "
			    "%s packet to server (will retry in %u seconds)",
			    pkt_name, ifsp->if_send_timeout / MILLISEC);
		return (0);
	}

	dhcpmsg(MSG_VERBOSE, "sent %s packet out %s", pkt_name,
	    ifsp->if_name);

	ifsp->if_packet_sent++;
	ifsp->if_sent++;
	return (1);
}

/*
 * send_pkt(): sends a packet out on an interface
 *
 *   input: struct ifslist *: the interface to send the packet out on
 *	    dhcp_pkt_t *: the packet to send out
 *	    in_addr_t: the destination IP address for the packet
 *	    stop_func_t *: a pointer to function to indicate when to stop
 *			   retransmitting the packet (if NULL, packet is
 *			   not retransmitted)
 *  output: int: 1 if the packet was sent, 0 otherwise
 */

int
send_pkt(struct ifslist *ifsp, dhcp_pkt_t *dpkt, in_addr_t dest,
    stop_func_t *stop)
{
	/*
	 * packets must be at least sizeof (PKT) or they may be dropped
	 * by routers.  pad out the packet in this case.
	 */

	dpkt->pkt_cur_len = MAX(dpkt->pkt_cur_len, sizeof (PKT));

	ifsp->if_packet_sent = 0;

	(void) memset(&ifsp->if_send_dest, 0, sizeof (ifsp->if_send_dest));
	ifsp->if_send_dest.sin_addr.s_addr = dest;
	ifsp->if_send_dest.sin_family	   = AF_INET;
	ifsp->if_send_dest.sin_port	   = htons(IPPORT_BOOTPS);
	ifsp->if_send_stop_func		   = stop;

	/*
	 * TODO: dispose of this gruesome assumption (there's no real
	 * technical gain from doing so, but it would be cleaner)
	 */

	assert(dpkt == &ifsp->if_send_pkt);

	/*
	 * clear out any packets which had been previously received
	 * but not pulled off of the recv_packet queue.
	 */

	free_pkt_list(&ifsp->if_recv_pkt_list);

	if (stop == NULL) {
		ifsp->if_retrans_timer = -1;
		ifsp->if_send_timeout = 0;	/* prevents retransmissions */
	} else
		ifsp->if_send_timeout = next_retransmission(0);

	return (send_pkt_internal(ifsp));
}

/*
 * retransmit(): retransmits the current packet on an interface
 *
 *   input: iu_tq_t *: unused
 *	    void *: the struct ifslist * to send the packet on
 *  output: void
 */

/* ARGSUSED */
static void
retransmit(iu_tq_t *tqp, void *arg)
{
	struct ifslist		*ifsp = (struct ifslist *)arg;

	if (check_ifs(ifsp) == 0) {
		(void) release_ifs(ifsp);
		return;
	}

	/*
	 * check the callback to see if we should keep sending retransmissions
	 */

	if (ifsp->if_send_stop_func(ifsp, ifsp->if_packet_sent))
		return;

	ifsp->if_send_timeout = next_retransmission(ifsp->if_send_timeout);
	(void) send_pkt_internal(ifsp);
}

/*
 * stop_pkt_retransmission(): stops retransmission of last sent packet
 *
 *   input: struct ifslist *: the interface to stop retransmission on
 *  output: void
 */

void
stop_pkt_retransmission(struct ifslist *ifsp)
{
	if (ifsp->if_retrans_timer != -1) {
		if (iu_cancel_timer(tq, ifsp->if_retrans_timer, NULL) == 1) {
			(void) release_ifs(ifsp);
			ifsp->if_retrans_timer = -1;
		}
	}
}

/*
 * recv_pkt(): receives packets on an interface (put on ifsp->if_recv_pkt_list)
 *
 *   input: struct ifslist *: the interface to receive packets on
 *	    int: the file descriptor to receive the packet on
 *	    dhcp_message_type_t: the types of packets to receive
 *	    boolean_t: if B_TRUE, more than one packet can be received
 *  output: int: 1 if a packet was received successfully, 0 otherwise
 */

int
recv_pkt(struct ifslist *ifsp, int fd, dhcp_message_type_t type,
    boolean_t chain)
{
	PKT_LIST	*plp;
	PKT		*pkt;
	ssize_t		retval;
	uchar_t		recv_pkt_type;
	const char	*recv_pkt_name;

	/*
	 * collect replies.  chain them up if the chain flag is set
	 * and we've already got one, otherwise drop the packet.
	 * calloc the PKT_LIST since dhcp_options_scan() relies on it
	 * being zeroed.
	 */

	pkt = calloc(1, ifsp->if_max);
	plp = calloc(1, sizeof (PKT_LIST));
	if (pkt == NULL || plp == NULL) {
		dhcpmsg(MSG_ERR, "recv_pkt: dropped packet");
		goto failure;
	}

	plp->pkt = pkt;

	switch (ifsp->if_state) {

	case BOUND:
	case RENEWING:
	case REBINDING:
		retval = recvfrom(fd, pkt, ifsp->if_max, 0, NULL, 0);
		break;

	default:
		retval = dlpi_recvfrom(fd, pkt, ifsp->if_max, 0);
		break;
	}

	if (retval == -1) {
		dhcpmsg(MSG_ERR, "recv_pkt: recvfrom failed, dropped");
		goto failure;
	}

	plp->len = retval;

	switch (dhcp_options_scan(plp, B_TRUE)) {

	case DHCP_WRONG_MSG_TYPE:
		dhcpmsg(MSG_WARNING, "recv_pkt: unexpected DHCP message");
		goto failure;

	case DHCP_GARBLED_MSG_TYPE:
		dhcpmsg(MSG_WARNING, "recv_pkt: garbled DHCP message type");
		goto failure;

	case DHCP_BAD_OPT_OVLD:
		dhcpmsg(MSG_WARNING, "recv_pkt: bad option overload");
		goto failure;

	case 0:
		break;

	default:
		dhcpmsg(MSG_WARNING, "recv_pkt: packet corrupted, dropped");
		goto failure;
	}

	/*
	 * make sure the packet we got in was one we were expecting --
	 * it needs to have the right type and to have the same xid.
	 */

	if (plp->opts[CD_DHCP_TYPE] != NULL)
		recv_pkt_type = *plp->opts[CD_DHCP_TYPE]->value;
	else
		recv_pkt_type = 0;

	recv_pkt_name = pkt_type_to_string(recv_pkt_type);

	if ((dhcp_type_ptob(recv_pkt_type) & type) == 0) {
		dhcpmsg(MSG_VERBOSE, "received unexpected %s packet on "
		    "%s, dropped", recv_pkt_name, ifsp->if_name);
		goto failure;
	}

	/* the xid is opaque -- no byteorder work */
	if (plp->pkt->xid != ifsp->if_send_pkt.pkt->xid) {
		dhcpmsg(MSG_VERBOSE, "received unexpected packet xid (%#x "
		    "instead of %#x) on %s, dropped", plp->pkt->xid,
		    ifsp->if_send_pkt.pkt->xid, ifsp->if_name);
		goto failure;
	}

	if (ifsp->if_recv_pkt_list != NULL) {
		if (chain == B_FALSE) {
			dhcpmsg(MSG_WARNING, "recv_pkt: unexpected additional "
			    "%s packet, dropped", recv_pkt_name);
			goto failure;
		}
	}

	dhcpmsg(MSG_VERBOSE, "received %s packet on %s", recv_pkt_name,
	    ifsp->if_name);

	prepend_to_pkt_list(&ifsp->if_recv_pkt_list, plp);
	ifsp->if_received++;
	return (1);

failure:
	free(pkt);
	free(plp);
	return (0);
}

/*
 * next_retransmission(): returns the number of seconds until the next
 *			  retransmission, based on the algorithm in RFC2131
 *
 *   input: uint32_t: the number of milliseconds for the last retransmission
 *  output: uint32_t: the number of milliseconds until the next retransmission
 */

static uint32_t
next_retransmission(uint32_t last_timeout_ms)
{
	uint32_t	timeout_ms;

	/*
	 * start at 4, and increase by a factor of 2 up to 64.  at each
	 * iteration, jitter the timeout by some fraction of a second.
	 */
	if (last_timeout_ms == 0)
		timeout_ms = 4 * MILLISEC;
	else
		timeout_ms = MIN(last_timeout_ms << 1, 64 * MILLISEC);

	return (timeout_ms + ((lrand48() % (2 * MILLISEC)) - MILLISEC));
}