view usr/src/cmd/cmd-inet/usr.sbin/ilbadm/ilbadm_subr.c @ 13622:e5889df1eaac

2077 lots of unreachable breaks in illumos gate Reviewed by: Dan McDonald <danmcd@nexenta.com> Reviewed by: Garrett D'Amore <garrett@damore.org> Approved by: Richard Lowe <richlowe@richlowe.net>
author Milan Jurik <milan.jurik@xylab.cz>
date Sat, 18 Feb 2012 19:52:02 +0100
parents 324bab2b3370
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 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 * Copyright 2012 Milan Jurik. All rights reserved.
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <ctype.h>
#include <assert.h>
#include <limits.h>
#include <libilb.h>
#include <libilb_impl.h>
#include "ilbadm.h"

#define	PORT_SEP	':'

typedef enum {
	numeric = 1,
	non_numeric
} addr_type_t;

ilbadm_val_type_t algo_types[] = {
	{(int)ILB_ALG_ROUNDROBIN, "roundrobin", "rr"},
	{(int)ILB_ALG_HASH_IP, "hash-ip", "hip"},
	{(int)ILB_ALG_HASH_IP_SPORT, "hash-ip-port", "hipp"},
	{(int)ILB_ALG_HASH_IP_VIP, "hash-ip-vip", "hipv"},
	{ILBD_BAD_VAL, NULL, NULL}
};

ilbadm_val_type_t topo_types[] = {
	{(int)ILB_TOPO_DSR, "DSR", "d"},
	{(int)ILB_TOPO_NAT, "NAT", "n"},
	{(int)ILB_TOPO_HALF_NAT, "HALF-NAT", "h"},
	{ILBD_BAD_VAL, NULL, NULL}
};

void
ip2str(ilb_ip_addr_t *ip, char *buf, size_t sz, int flags)
{
	int	len;

	switch (ip->ia_af) {
	case AF_INET:
		if (*(uint32_t *)&ip->ia_v4 == 0)
			buf[0] = '\0';
		else
			(void) inet_ntop(AF_INET, (void *)&ip->ia_v4, buf, sz);
		break;
	case AF_INET6:
		if (IN6_IS_ADDR_UNSPECIFIED(&ip->ia_v6)) {
			buf[0] = '\0';
			break;
		}
		if (!(flags & V6_ADDRONLY))
			*buf++ = '[';
		sz--;
		(void) inet_ntop(ip->ia_af, (void *)&ip->ia_v6, buf, sz);
		if (!(flags & V6_ADDRONLY)) {
			len = strlen(buf);
			buf[len] = ']';
			buf[++len] = '\0';
		}
		break;
	default: buf[0] = '\0';
	}
}

char *
i_str_from_val(int val, ilbadm_val_type_t *types)
{
	ilbadm_val_type_t	*v;

	for (v = types; v->v_type != ILBD_BAD_VAL; v++) {
		if (v->v_type == val)
			break;
	}
	/* we return this in all cases */
	return (v->v_name);
}

int
i_val_from_str(char *name, ilbadm_val_type_t *types)
{
	ilbadm_val_type_t	*v;

	for (v = types; v->v_type != ILBD_BAD_VAL; v++) {
		if (strncasecmp(name, v->v_name, sizeof (v->v_name)) == 0 ||
		    strncasecmp(name, v->v_alias, sizeof (v->v_alias)) == 0)
			break;
	}
	/* we return this in all cases */
	return (v->v_type);
}

ilbadm_key_code_t
i_match_key(char *key, ilbadm_key_name_t *keylist)
{
	ilbadm_key_name_t	*t_key;

	for (t_key = keylist; t_key->k_key != ILB_KEY_BAD; t_key++) {
		if (strncasecmp(key, t_key->k_name,
		    sizeof (t_key->k_name)) == 0 ||
		    strncasecmp(key, t_key->k_alias,
		    sizeof (t_key->k_alias)) == 0)
			break;
	}
	return (t_key->k_key);
}

/*
 * try to match:
 * 1) IPv4 address
 * 2) IPv6 address
 * 3) a hostname
 */
static ilbadm_status_t
i_match_onehost(const char *val, ilb_ip_addr_t *ip, addr_type_t *a_type)
{
	struct addrinfo *ai = NULL;
	struct addrinfo hints;
	addr_type_t	at = numeric;

	(void) memset((void *)&hints, 0, sizeof (hints));
	hints.ai_flags |= AI_NUMERICHOST;

	/*
	 * if *a_type == numeric, we only want to check whether this
	 * is a (valid) numeric IP address. If we do and it is NOT,
	 * we return _ENOENT.
	 */
	if (getaddrinfo(val, NULL, &hints, &ai) != 0) {
		if (a_type != NULL && (*a_type == numeric))
			return (ILBADM_INVAL_ADDR);

		at = non_numeric;
		if (getaddrinfo(val, NULL, NULL, &ai) != 0)
			return (ILBADM_INVAL_ADDR);
	}

	ip->ia_af = ai->ai_family;
	switch (ip->ia_af) {
	case AF_INET: {
		struct sockaddr_in	sa;

		assert(ai->ai_addrlen == sizeof (sa));
		(void) memcpy(&sa, ai->ai_addr, sizeof (sa));
		ip->ia_v4 = sa.sin_addr;
		break;
	}
	case AF_INET6: {
		struct sockaddr_in6	sa;

		assert(ai->ai_addrlen == sizeof (sa));
		(void) memcpy(&sa, ai->ai_addr, sizeof (sa));
		ip->ia_v6 = sa.sin6_addr;
		break;
	}
	default:
		return (ILBADM_INVAL_AF);
	}

	if (a_type != NULL)
		*a_type = at;
	return (ILBADM_OK);
}

static ilbadm_status_t
i_store_serverID(void *store, char *val)
{
	ilbadm_servnode_t	*s = (ilbadm_servnode_t *)store;
	ilb_server_data_t	*sn = &s->s_spec;

	/*
	 * we shouldn't need to check for length here, as a name that's
	 * too long won't exist in the system anyway.
	 */
	(void) strlcpy(sn->sd_srvID, val, sizeof (sn->sd_srvID));
	return (ILBADM_OK);
}

static struct in_addr
i_next_in_addr(struct in_addr *a, int dir)
{
	struct in_addr	new_in;
	uint32_t	iah;

	iah = ntohl(a->s_addr);
	if (dir == 1)
		iah++;
	else
		iah--;
	new_in.s_addr = htonl(iah);
	return (new_in);
}

static ilbadm_status_t
i_expand_ipv4range(ilbadm_sgroup_t *sg, ilb_server_data_t *srv,
    ilb_ip_addr_t *ip1, ilb_ip_addr_t *ip2)
{
	struct in_addr	*a1;
	ilbadm_servnode_t	*sn_new;
	ilb_ip_addr_t	new_ip;

	a1 = &ip1->ia_v4;

	new_ip.ia_af = AF_INET;
	new_ip.ia_v4 = i_next_in_addr(a1, 1);
	while (ilb_cmp_ipaddr(&new_ip, ip2, NULL) < 1) {
		sn_new = i_new_sg_elem(sg);
		sn_new->s_spec.sd_addr = new_ip;
		sn_new->s_spec.sd_minport = srv->sd_minport;
		sn_new->s_spec.sd_maxport = srv->sd_maxport;
		new_ip.ia_v4 = i_next_in_addr(&new_ip.ia_v4, 1);
	}
	return (ILBADM_OK);
}

static struct in6_addr
i_next_in6_addr(struct in6_addr *a, int dir)
{
	struct in6_addr	ia6;
	uint64_t	al, ah;

	ah = INV6_N2H_MSB64(a);
	al = INV6_N2H_LSB64(a);

	if (dir == 1) {
		/* overflow */
		if (++al == 0)
			ah++;
	} else {
		/* underflow */
		if (--al == 0xffffffff)
			ah--;
	}

	INV6_H2N_MSB64(&ia6, ah);
	INV6_H2N_LSB64(&ia6, al);
	return (ia6);
}


static ilbadm_status_t
i_expand_ipv6range(ilbadm_sgroup_t *sg, ilb_server_data_t *srv,
    ilb_ip_addr_t *ip1, ilb_ip_addr_t *ip2)
{
	struct in6_addr	*a1;
	ilbadm_servnode_t	*sn_new;
	ilb_ip_addr_t	new_ip;

	a1 = &ip1->ia_v6;

	new_ip.ia_af = AF_INET6;
	new_ip.ia_v6 = i_next_in6_addr(a1, 1);
	while (ilb_cmp_ipaddr(&new_ip, ip2, NULL) < 1) {
		sn_new = i_new_sg_elem(sg);
		sn_new->s_spec.sd_addr = new_ip;
		sn_new->s_spec.sd_minport = srv->sd_minport;
		sn_new->s_spec.sd_maxport = srv->sd_maxport;
		new_ip.ia_v6 = i_next_in6_addr(&new_ip.ia_v6, 1);
	}
	return (ILBADM_OK);
}


/*
 * we create a list node in the servergroup for every ip address
 * in the range [ip1, ip2], where we interpret the ip addresses as
 * numbers
 * the first ip address is already stored in "sn"
 */
static ilbadm_status_t
i_expand_iprange(ilbadm_sgroup_t *sg, ilb_server_data_t *sr,
    ilb_ip_addr_t *ip1, ilb_ip_addr_t *ip2)
{
	int		cmp;
	int64_t		delta;

	if (ip2->ia_af == 0)
		return (ILBADM_OK);

	if (ip1->ia_af != ip2->ia_af) {
		ilbadm_err(gettext("IP address mismatch"));
		return (ILBADM_LIBERR);
	}

	/* if ip addresses are the same, we're done */
	if ((cmp = ilb_cmp_ipaddr(ip1, ip2, &delta)) == 0)
		return (ILBADM_OK);
	if (cmp == 1) {
		ilbadm_err(gettext("starting IP address is must be less"
		    " than ending ip address in ip range specification"));
		return (ILBADM_LIBERR);
	}

	/* if the implicit number of IPs is too large, stop */
	if (abs((int)delta) > MAX_IP_SPREAD)
		return (ILBADM_TOOMANYIPADDR);

	switch (ip1->ia_af) {
	case AF_INET:
		return (i_expand_ipv4range(sg, sr, ip1, ip2));
	case AF_INET6:
		return (i_expand_ipv6range(sg, sr, ip1, ip2));
	}
	return (ILBADM_INVAL_AF);
}

/*
 * parse a port spec (number or by service name) and
 * return the numeric port in *host* byte order
 *
 * Upon return, *flags contains ILB_FLAGS_SRV_PORTNAME if a service name matches
 */
static int
i_parseport(char *port, char *proto, int *flags)
{
	struct servent	*se;

	/* assumption: port names start with a non-digit */
	if (isdigit(port[0])) {
		if (flags != NULL)
			*flags &= ~ILB_FLAGS_SRV_PORTNAME;
		return ((int)strtol(port, NULL, 10));
	}

	se = getservbyname(port, proto);
	if (se == NULL)
		return (-1);

	if (flags != NULL)
		*flags |= ILB_FLAGS_SRV_PORTNAME;

	/*
	 * we need to convert to host byte order to be in sync with
	 * numerical ports. since result needs to be compared, this
	 * is preferred to returning NW byte order
	 */
	return ((int)(ntohs(se->s_port)));
}

/*
 * matches one hostname or IP address and stores it in "store".
 * space must have been pre-allocated to accept data
 * "sg" != NULL only for cases where ip ranges may be coming in.
 */
static ilbadm_status_t
i_match_hostorip(void *store, ilbadm_sgroup_t *sg, char *val,
    int flags, ilbadm_key_code_t keyword)
{
	boolean_t	is_ip_range_ok = flags & OPT_IP_RANGE;
	boolean_t	is_addr_numeric = flags & OPT_NUMERIC_ONLY;
	boolean_t	is_ports_ok = flags & OPT_PORTS;
	boolean_t	ports_only = flags & OPT_PORTS_ONLY;
	boolean_t	is_nat_src = flags & OPT_NAT;
	char		*port_pref, *dash;
	char		*port1p, *port2p, *host2p, *host1p;
	char		*close1, *close2;
	ilb_ip_addr_t	ip2store;
	ilb_ip_addr_t	*ip1, *ip2;
	int		p1, p2;
	ilb_server_data_t	*s = NULL;
	ilbadm_status_t	rc = ILBADM_OK;
	int		af = AF_INET;
	addr_type_t	at = 0;
	int		p_flg;
	struct in6_addr v6nameaddr;

	port1p = port2p = host2p = host1p =  NULL;
	port_pref = dash = NULL;
	close1 = close2 = NULL;
	errno = 0;

	if (is_nat_src) {
		ilb_rule_data_t *rd = (ilb_rule_data_t *)store;

		ip1 = &rd->r_nat_src_start;
		ip2 = &rd->r_nat_src_end;
	} else {
		ilbadm_servnode_t *sn = (ilbadm_servnode_t *)store;

		s = &sn->s_spec;
		ip1 = &s->sd_addr;
		ip2 = &ip2store;
		bzero(ip2, sizeof (*ip2));
	}

	if (ports_only) {
		is_ports_ok = B_TRUE;
		port_pref = val - 1; /* we increment again later on */
		goto ports;
	}

	/*
	 * we parse the syntax ip[-ip][:port[-port]]
	 * since IPv6 addresses contain ':'s as well, they need to be
	 * enclosed in "[]" to be distinct from a potential port spec.
	 * therefore, we need to first check whether we're dealing with
	 * IPv6 addresses before we can go search for the port seperator
	 * and ipv6 range could look like this: [ff::0]-[ff::255]:80
	 */
	if ((keyword == ILB_KEY_SERVER) && (strchr(val, ':') != NULL) &&
	    (*val != '[') && ((inet_pton(AF_INET6, val, &v6nameaddr)) != 0)) {
			/*
			 * V6 addresses must be enclosed within
			 * brackets when specifying server addresses
			 */
			rc = ILBADM_INVAL_SYNTAX;
			goto err_out;
	}

	if (*val == '[') {
		af = AF_INET6;

		val++;
		host1p = val;

		close1 = strchr(val, (int)']');
		if (close1 == NULL) {
			rc = ILBADM_INVAL_SYNTAX;
			goto err_out;
		}
		*close1 = '\0';
		at = 0;
		rc = i_match_onehost(host1p, ip1, &at);
		if (rc != ILBADM_OK)
			goto err_out;
		if (at != numeric) {
			rc = ILBADM_INVAL_ADDR;
			goto err_out;
		}
		if (ip1->ia_af != af) {
			rc = ILBADM_INVAL_AF;
			goto err_out;
		}
		val = close1 + 1;

		if (*val == PORT_SEP) {
			port_pref = val;
			goto ports;
		}
		if (*val == '-') {
			dash = val;
			if (!is_ip_range_ok) {
				ilbadm_err(gettext("port ranges not allowed"));
				rc = ILBADM_LIBERR;
				goto err_out;
			}
			val++;
			if (*val != '[') {
				rc = ILBADM_INVAL_SYNTAX;
				goto err_out;
			}
			val++;
			close2 = strchr(val, (int)']');
			if (close2 == NULL) {
				rc = ILBADM_INVAL_SYNTAX;
				goto err_out;
			}
			*close2 = '\0';
			host2p = val;
			at = 0;
			rc = i_match_onehost(host2p, ip2, &at);
			if (rc != ILBADM_OK)
				goto err_out;
			if (at != numeric) {
				rc = ILBADM_INVAL_ADDR;
				goto err_out;
			}
			if (ip2->ia_af != af) {
				rc = ILBADM_INVAL_AF;
				goto err_out;
			}
			val = close2+1;
		}
	}

	/* ports always potentially allow ranges - XXXms: check? */
	port_pref = strchr(val, (int)PORT_SEP);
ports:
	if (port_pref != NULL && is_ports_ok) {
		port1p = port_pref + 1;
		*port_pref = '\0';

		dash = strchr(port1p, (int)'-');
		if (dash != NULL) {
			port2p = dash + 1;
			*dash = '\0';
		}
		if (port1p != NULL) {
			p1 = i_parseport(port1p, NULL, &p_flg);
			if (p1 == -1 || p1 == 0 || p1 > ILB_MAX_PORT) {
				ilbadm_err(gettext("invalid port value %s"
				    " specified"), port1p);
				rc = ILBADM_LIBERR;
				goto err_out;
			}
			s->sd_minport = htons((in_port_t)p1);
			if (p_flg & ILB_FLAGS_SRV_PORTNAME)
				s->sd_flags |= ILB_FLAGS_SRV_PORTNAME;
		}
		if (port2p != NULL) {
			/* ranges are only allowed for numeric ports */
			if (p_flg & ILB_FLAGS_SRV_PORTNAME) {
				ilbadm_err(gettext("ranges are only allowed"
				    " for numeric ports"));
				rc = ILBADM_LIBERR;
				goto err_out;
			}
			p2 = i_parseport(port2p, NULL, &p_flg);
			if (p2 == -1 || p2 <= p1 || p2 > ILB_MAX_PORT ||
			    (p_flg & ILB_FLAGS_SRV_PORTNAME) ==
			    ILB_FLAGS_SRV_PORTNAME) {
				ilbadm_err(gettext("invalid port value %s"
				    " specified"), port2p);
				rc = ILBADM_LIBERR;
				goto err_out;
			}
			s->sd_maxport = htons((in_port_t)p2);
		}
		/*
		 * we fill the '-' back in, but not the port seperator,
		 * as the \0 in its place terminates the ip address(es)
		 */
		if (dash != NULL)
			*dash = '-';
		if (ports_only)
			goto out;
	}

	if (af == AF_INET6)
		goto out;

	/*
	 * we need to handle these situations for hosts:
	 *   a. ip address
	 *   b. ip address range (ip1-ip2)
	 *   c. a hostname (may include '-' or start with a digit)
	 *
	 * We want to do hostname lookup only if we're quite sure that
	 * we actually are looking at neither a single IP address nor a
	 * range of same, as this can hang if name service is not set up
	 * (sth. likely in a LB environment).
	 *
	 * here's how we proceed:
	 * 1. try to match numeric only. If that succeeds, we're done.
	 *    (getaddrinfo, which we call in i_match_onehost(), fails if
	 *    it encounters a '-')
	 * 2. search for a '-'; if we find one, try numeric match for
	 *    both sides. if this fails:
	 * 3. re-insert '-' and try for a legal hostname.
	 */
	/* 1. */
	at = numeric;
	rc = i_match_onehost(val, ip1, &at);
	if (rc == ILBADM_OK)
		goto out;

	/* 2. */
	dash = strchr(val, (int)'-');
	if (dash != NULL && is_ip_range_ok) {
		host2p = dash + 1;
		*dash = '\0';
		at = numeric;
		rc = i_match_onehost(host2p, ip2, &at);
		if (rc != ILBADM_OK || at != numeric) {
			*dash = '-';
			dash = NULL;
			bzero(ip2, sizeof (*ip2));
			goto hostname;
		}
		/*
		 * if the RHS of '-' is an IP but LHS is not, we might
		 * have a hostname of form x-y where y is just a number
		 * (this seems a valid IPv4 address), so we need to
		 * try a complete hostname
		 */
		rc = i_match_onehost(val, ip1, &at);
		if (rc != ILBADM_OK || at != numeric) {
			*dash = '-';
			dash = NULL;
			goto hostname;
		}
		goto out;
	}
hostname:
	/* 3. */

	if (is_addr_numeric)
		at = numeric;
	else
		at = 0;
	rc = i_match_onehost(val, ip1, &at);
	if (rc != ILBADM_OK) {
		goto out;
	}
	if (s != NULL) {
		s->sd_flags |= ILB_FLAGS_SRV_HOSTNAME;
		/* XXX: todo: save hostname for re-display for admin */
	}

out:
	if (dash != NULL && !is_nat_src) {
		rc = i_expand_iprange(sg, s, ip1, ip2);
		if (rc != ILBADM_OK)
			goto err_out;
	}

	if (is_nat_src && host2p == NULL)
		*ip2 = *ip1;

err_out:
	/*
	 * we re-insert what we overwrote, especially in the error case
	 */
	if (close2 != NULL)
		*close2 = ']';
	if (close1 != NULL)
		*close1 = '[';
	if (dash != NULL)
		*dash = '-';
	if (port_pref != NULL && !ports_only)
		*port_pref = PORT_SEP;

	return (rc);
}

/*
 * type-agnostic helper function to return a pointer to a
 * pristine (and maybe freshly allocated) piece of storage
 * ready for something fitting "key"
 */
static void *
i_new_storep(void *store, ilbadm_key_code_t key)
{
	void	*res;

	switch (key) {
	case ILB_KEY_SERVER:
	case ILB_KEY_SERVRANGE:
	case ILB_KEY_SERVERID:
		res = (void *) i_new_sg_elem(store);
		break;
	default: res = NULL;
		break;
	}

	return (res);
}

/*
 * make sure everything that needs to be there is there
 */
ilbadm_status_t
i_check_rule_spec(ilb_rule_data_t *rd)
{
	int32_t		vip_af = rd->r_vip.ia_af;
	ilb_ip_addr_t	*prxy_src;

	if (vip_af != AF_INET && vip_af != AF_INET6)
		return (ILBADM_INVAL_AF);

	if (*rd->r_sgname == '\0')
		return (ILBADM_ENOSGNAME);

	if (rd->r_algo == 0 || rd->r_topo == 0) {
		ilbadm_err(gettext("lbalg or type is unspecified"));
		return (ILBADM_LIBERR);
	}

	if (rd->r_topo == ILB_TOPO_NAT) {
		prxy_src = &rd->r_nat_src_start;
		if (prxy_src->ia_af != vip_af) {
			ilbadm_err(gettext("proxy-src is either missing"
			    " or its address family does not"
			    " match that of the VIP address"));
			return (ILBADM_LIBERR);
		}
	}
	/* extend as necessary */

	return (ILBADM_OK);
}

/*
 * in parameter "sz" describes size (in bytes) of mask
 */
static int
mask_to_prefixlen(const uchar_t *mask, const int sz)
{
	uchar_t	c;
	int	i, j;
	int	len = 0;
	int	tmask;

	/*
	 * for every byte in the mask, we start with most significant
	 * bit and work our way down to the least significant bit; as
	 * long as we find the bit set, we add 1 to the length. the
	 * first unset bit we encounter terminates this process
	 */
	for (i = 0; i < sz; i++) {
		c = mask[i];
		tmask = 1 << 7;
		for (j = 7; j >= 0; j--) {
			if ((c & tmask) == 0)
				return (len);
			len++;
			tmask >>= 1;
		}
	}
	return (len);
}

int
ilbadm_mask_to_prefixlen(ilb_ip_addr_t *ip)
{
	int af = ip->ia_af;
	int len = 0;

	assert(af == AF_INET || af == AF_INET6);
	switch (af) {
	case AF_INET:
		len = mask_to_prefixlen((uchar_t *)&ip->ia_v4.s_addr,
		    sizeof (ip->ia_v4));
		break;
	case AF_INET6:
		len = mask_to_prefixlen((uchar_t *)&ip->ia_v6.s6_addr,
		    sizeof (ip->ia_v6));
		break;
	}
	return (len);
}

/* copied from ifconfig.c, changed to return symbolic constants */
/*
 * Convert a prefix length to a mask.
 * Returns 1 if ok. 0 otherwise.
 * Assumes the mask array is zero'ed by the caller.
 */
static boolean_t
in_prefixlentomask(int prefixlen, int maxlen, uchar_t *mask)
{
	if (prefixlen < 0 || prefixlen > maxlen)
		return (B_FALSE);

	while (prefixlen > 0) {
		if (prefixlen >= 8) {
			*mask++ = 0xFF;
			prefixlen -= 8;
			continue;
		}
		*mask |= 1 << (8 - prefixlen);
		prefixlen--;
	}
	return (B_TRUE);
}

ilbadm_status_t
ilbadm_set_netmask(char *val, ilb_ip_addr_t *ip, int af)
{
	int	prefixlen, maxval;
	boolean_t	r;
	char	*end;

	assert(af == AF_INET || af == AF_INET6);

	maxval = (af == AF_INET) ? 32 : 128;

	if (*val == '/')
		val++;
	prefixlen = strtol(val, &end, 10);
	if ((val == end) || (*end != '\0')) {
		ilbadm_err(gettext("invalid pmask provided"));
		return (ILBADM_LIBERR);
	}

	if (prefixlen < 1 || prefixlen > maxval) {
		ilbadm_err(gettext("invalid pmask provided (AF mismatch?)"));
		return (ILBADM_LIBERR);
	}

	switch (af) {
	case AF_INET:
		r = in_prefixlentomask(prefixlen, maxval,
		    (uchar_t *)&ip->ia_v4.s_addr);
		break;
	case AF_INET6:
		r = in_prefixlentomask(prefixlen, maxval,
		    (uchar_t *)&ip->ia_v6.s6_addr);
		break;
	}
	if (r != B_TRUE) {
		ilbadm_err(gettext("cannot convert %s to a netmask"), val);
		return (ILBADM_LIBERR);
	}
	ip->ia_af = af;
	return (ILBADM_OK);
}

static ilbadm_status_t
i_store_val(char *val, void *store, ilbadm_key_code_t keyword)
{
	ilbadm_status_t	rc = ILBADM_OK;
	void		*storep = store;
	ilb_rule_data_t	*rd = NULL;
	ilbadm_sgroup_t	*sg = NULL;
	ilb_hc_info_t	*hc_info = NULL;
	struct protoent	*pe;
	int64_t		tmp_val;

	if (*val == '\0')
		return (ILBADM_NOKEYWORD_VAL);

	/* some types need new storage, others don't */
	switch (keyword) {
	case ILB_KEY_SERVER:
	case ILB_KEY_SERVERID:
		sg = (ilbadm_sgroup_t *)store;
		storep = i_new_storep(store, keyword);
		break;
	case ILB_KEY_HEALTHCHECK:
	case ILB_KEY_SERVERGROUP:
		rd = (ilb_rule_data_t *)store;
		break;
	case ILB_KEY_VIP:	/* fallthrough */
	case ILB_KEY_PORT:	/* fallthrough */
	case ILB_KEY_HCPORT:	/* fallthrough */
	case ILB_KEY_CONNDRAIN:	/* fallthrough */
	case ILB_KEY_NAT_TO:	/* fallthrough */
	case ILB_KEY_STICKY_TO:	/* fallthrough */
	case ILB_KEY_PROTOCOL:	/* fallthrough */
	case ILB_KEY_ALGORITHM:	/* fallthrough */
	case ILB_KEY_STICKY:	/* fallthrough */
	case ILB_KEY_TYPE:	/* fallthrough */
	case ILB_KEY_SRC:	/* fallthrough */
		rd = (ilb_rule_data_t *)store;
		break;
	case ILB_KEY_HC_TEST:
	case ILB_KEY_HC_COUNT:
	case ILB_KEY_HC_INTERVAL:
	case ILB_KEY_HC_TIMEOUT:
		hc_info = (ilb_hc_info_t *)store;
	default: /* do nothing */
		;
	}

	switch (keyword) {
	case ILB_KEY_SRC:
		/*
		 * the proxy-src keyword is only valid for full NAT topology
		 * the value is either a single or a range of IP addresses.
		 */
		if (rd->r_topo != ILB_TOPO_NAT) {
			rc = ILBADM_INVAL_PROXY;
			break;
		}
		rc = i_match_hostorip(storep, sg, val, OPT_NUMERIC_ONLY |
		    OPT_IP_RANGE | OPT_NAT, ILB_KEY_SRC);
		break;
	case ILB_KEY_SERVER:
		rc = i_match_hostorip(storep, sg, val,
		    OPT_IP_RANGE | OPT_PORTS, ILB_KEY_SERVER);
		break;
	case ILB_KEY_SERVERID:
		if (val[0] != ILB_SRVID_PREFIX)
			rc = ILBADM_INVAL_SRVID;
		else
			rc = i_store_serverID(storep, val);
		break;
	case ILB_KEY_VIP: {
		ilb_ip_addr_t	*vip = &rd->r_vip;
		addr_type_t	at = numeric;
		char		*close = NULL;

		/*
		 * we duplicate some functionality of i_match_hostorip
		 * here; that function is geared to mandate '[]' for IPv6
		 * addresses, which we want to relax here, so as not to
		 * make i_match_hostorip even longer, we do what we need
		 * here.
		 */
		if (*val == '[') {
			val++;
			if ((close = strchr(val, (int)']')) == NULL) {
				rc = ILBADM_INVAL_SYNTAX;
				break;
			}
			*close = NULL;
		}
		rc = i_match_onehost(val, vip, &at);
		/* re-assemble string as we found it */
		if (close != NULL) {
			*close = ']';
			if (rc == ILBADM_OK && vip->ia_af != AF_INET6) {
				ilbadm_err(gettext("use of '[]' only valid"
				    " with IPv6 addresses"));
				rc = ILBADM_LIBERR;
			}
		}
		break;
	}
	case ILB_KEY_CONNDRAIN:
		tmp_val = strtoll(val, NULL, 10);
		if (tmp_val <= 0 || tmp_val > UINT_MAX) {
			rc = ILBADM_EINVAL;
			break;
		}
		rd->r_conndrain = tmp_val;
		break;
	case ILB_KEY_NAT_TO:
		tmp_val = strtoll(val, NULL, 10);
		if (tmp_val < 0 || tmp_val > UINT_MAX) {
			rc = ILBADM_EINVAL;
			break;
		}
		rd->r_nat_timeout = tmp_val;
		break;
	case ILB_KEY_STICKY_TO:
		tmp_val = strtoll(val, NULL, 10);
		if (tmp_val <= 0 || tmp_val > UINT_MAX) {
			rc = ILBADM_EINVAL;
			break;
		}
		rd->r_sticky_timeout = tmp_val;
		break;
	case ILB_KEY_PORT:
		if (isdigit(*val)) {
			ilbadm_servnode_t	sn;

			bzero(&sn, sizeof (sn));
			rc = i_match_hostorip((void *)&sn, sg, val,
			    OPT_PORTS_ONLY, ILB_KEY_PORT);
			if (rc != ILBADM_OK)
				break;
			rd->r_minport = sn.s_spec.sd_minport;
			rd->r_maxport = sn.s_spec.sd_maxport;
		} else {
			struct servent	*se;

			se = getservbyname(val, NULL);
			if (se == NULL) {
				rc = ILBADM_ENOSERVICE;
				break;
			}
			rd->r_minport = se->s_port;
			rd->r_maxport = 0;
		}
		break;
	case ILB_KEY_HCPORT:
		if (isdigit(*val)) {
			int hcport = atoi(val);

			if (hcport < 1 || hcport > 65535) {
				ilbadm_err(gettext("illegal number for"
				    " hcport %s"), val);
				rc = ILBADM_LIBERR;
				break;
			}
			rd->r_hcport = htons(hcport);
			rd->r_hcpflag = ILB_HCI_PROBE_FIX;
		} else if (strcasecmp(val, "ANY") == 0) {
			rd->r_hcport = 0;
			rd->r_hcpflag = ILB_HCI_PROBE_ANY;
		} else {
			return (ILBADM_EINVAL);
		}
		break;
	case ILB_KEY_PROTOCOL:
		pe = getprotobyname(val);
		if (pe == NULL)
			rc = ILBADM_ENOPROTO;
		else
			rd->r_proto = pe->p_proto;
		break;
	case ILB_KEY_ALGORITHM:
		rd->r_algo = i_val_from_str(val, &algo_types[0]);
		if (rd->r_algo == ILBD_BAD_VAL)
			rc = ILBADM_INVAL_ALG;
		break;
	case ILB_KEY_STICKY:
		rd->r_flags |= ILB_FLAGS_RULE_STICKY;
		/*
		 * CAVEAT: the use of r_vip.ia_af implies that the VIP
		 * *must* be specified on the commandline *before*
		 * the sticky mask.
		 */
		if (AF_UNSPEC == rd->r_vip.ia_af) {
			ilbadm_err(gettext("option '%s' requires that VIP be "
			    "specified first"), ilbadm_key_to_opt(keyword));
			rc = ILBADM_LIBERR;
			break;
		}
		rc = ilbadm_set_netmask(val, &rd->r_stickymask,
		    rd->r_vip.ia_af);
		break;
	case ILB_KEY_TYPE:
		rd->r_topo = i_val_from_str(val, &topo_types[0]);
		if (rd->r_topo == ILBD_BAD_VAL)
			rc = ILBADM_INVAL_OPER;
		break;
	case ILB_KEY_SERVERGROUP:
		(void) strlcpy(rd->r_sgname, (char *)val,
		    sizeof (rd->r_sgname));
		break;
	case ILB_KEY_HEALTHCHECK:
		(void) strlcpy(rd->r_hcname, (char *)val,
		    sizeof (rd->r_hcname));
		break;
	case ILB_KEY_HC_TEST:
		(void) strlcpy(hc_info->hci_test, (char *)val,
		    sizeof (hc_info->hci_test));
		break;
	case ILB_KEY_HC_COUNT:
		if (isdigit(*val))
			hc_info->hci_count = atoi(val);
		else
			return (ILBADM_EINVAL);
		break;
	case ILB_KEY_HC_INTERVAL:
		if (isdigit(*val))
			hc_info->hci_interval = atoi(val);
		else
			return (ILBADM_EINVAL);
		break;
	case ILB_KEY_HC_TIMEOUT:
		if (isdigit(*val))
			hc_info->hci_timeout = atoi(val);
		else
			return (ILBADM_EINVAL);
		break;
	default: rc = ILBADM_INVAL_KEYWORD;
		break;
	}

	return (rc);
}

/*
 * generic parsing function.
 * parses "key=value[,value]" strings in "arg". keylist determines the
 * list of valid keys in the LHS. keycode determines interpretation and
 * storage in store
 * XXXms: looks like "key=value[,value]" violates spec. needs a fix
 */
ilbadm_status_t
i_parse_optstring(char *arg, void *store, ilbadm_key_name_t *keylist,
    int flags, int *count)
{
	ilbadm_status_t	rc = ILBADM_OK;
	char		*comma = NULL, *equals = NULL;
	char		*key, *nextkey, *val;
	ilbadm_key_code_t	keyword;
	boolean_t	is_value_list = flags & OPT_VALUE_LIST;
	boolean_t	assign_seen = B_FALSE;
	int		n;

	key = arg;
	n = 1;
	/*
	 * Algorithm:
	 * 1. find any commas indicating and seperating current value
	 *    from a following value
	 * 2. if we're expecting a list of values (seperated by commas)
	 *	and have already seen the assignment, then
	 *	get the next "value"
	 * 3. else (we're looking at the first element of the RHS)
	 *	4. find the '='
	 *	5. match the keyword to the list we were passed in
	 * 6. store the value.
	 */
	while (key != NULL && *key != '\0') {
		comma = equals = NULL;

		/* 2 */
		nextkey = strchr(key, (int)',');
		if (nextkey != NULL) {
			comma = nextkey++;
			*comma = '\0';
		}

		/* 3a */
		if (is_value_list && assign_seen) {
			val = key;
		/* 3b */
		} else {
			/* 4 */
			equals = strchr(key, (int)'=');
			if (equals == NULL) {
				ilbadm_err("%s: %s", key,
				    ilbadm_errstr(ILBADM_ASSIGNREQ));
				rc = ILBADM_LIBERR;
				goto out;
			}
			val = equals + 1;
			*equals = '\0';
			assign_seen = B_TRUE;

			/* 5 */
			keyword = i_match_key(key, keylist);
			if (keyword == ILB_KEY_BAD) {
				ilbadm_err(gettext("bad keyword %s"), key);
				rc = ILBADM_LIBERR;
				goto out;
			}
		}

		/* 6 */
		rc = i_store_val(val, store, keyword);
		if (rc != ILBADM_OK) {
			ilbadm_err("%s: %s", key, ilbadm_errstr(rc));
			/* Change to ILBADM_ILBERR to avoid more err msgs. */
			rc = ILBADM_LIBERR;
			goto out;
		}

		key = nextkey;
		n++;
	}

out:
	if (comma != NULL)
		*comma = ',';
	if (equals != NULL)
		*equals = '=';
	if (count != NULL)
		*count = n;
	return (rc);
}