view src/director/mail-host.c @ 18067:a7e830b9b967

director: Added support for backend cluster "tags". This allows using a single director ring for multiple backend clusters. By default everything has an empty tag. A passdb lookup can return "director_tag" field containing the wanted tag name. If there aren't any backend servers with the wanted tag, it's treated the same as if there aren't any backend servers available (= wait for 30 secs for a backend and then return temporary failure). Tags can be added to configuration by adding @tag suffix to IPs/hosts. For example: director_mail_servers = 10.0.0.100-10.0.0.110@name1 10.0.0.120@name2 "doveadm director add" can also add tags either with @tag suffix or with -t parameter. "doveadm director status user@domain" requires giving the user's correct tag with -t parameter or the results won't be correct (empty tag's results are shown). Tags can't currently be changed for an existing host without removing it first.
author Timo Sirainen <tss@iki.fi>
date Wed, 12 Nov 2014 06:58:37 +0200
parents b9df3d654710
children 3009a1a6f6d5
line wrap: on
line source

/* Copyright (c) 2010-2014 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "bsearch-insert-pos.h"
#include "md5.h"
#include "mail-host.h"

#define VHOST_MULTIPLIER 100

struct mail_vhost {
	unsigned int hash;
	struct mail_host *host;
};

struct mail_host_list {
	ARRAY_TYPE(mail_host) hosts;
	ARRAY(struct mail_vhost) vhosts;
	bool hosts_unsorted;
	bool consistent_hashing;
};

static int
mail_host_cmp(struct mail_host *const *h1, struct mail_host *const *h2)
{
	return net_ip_cmp(&(*h1)->ip, &(*h2)->ip);
}

static int
mail_vhost_cmp(const struct mail_vhost *h1, const struct mail_vhost *h2)
{
	if (h1->hash < h2->hash)
		return -1;
	else if (h1->hash > h2->hash)
		return 1;
	/* hash collision. not ideal, but we'll need to keep the order
	   consistent across directors so compare the IPs next. */
	return net_ip_cmp(&h1->host->ip, &h2->host->ip);
}

static int
mail_vhost_hash_cmp(const unsigned int *hash, const struct mail_vhost *vhost)
{
	if (vhost->hash < *hash)
		return 1;
	else if (vhost->hash > *hash)
		return -1;
	else
		return 0;
}

static void mail_vhost_add(struct mail_host_list *list, struct mail_host *host)
{
	struct mail_vhost *vhost;
	struct md5_context md5_ctx, md5_ctx2;
	unsigned char md5[MD5_RESULTLEN];
	const char *ip_str;
	char num_str[MAX_INT_STRLEN];
	unsigned int i, j;

	ip_str = net_ip2addr(&host->ip);

	md5_init(&md5_ctx);
	md5_update(&md5_ctx, ip_str, strlen(ip_str));

	for (i = 0; i < host->vhost_count; i++) {
		md5_ctx2 = md5_ctx;
		i_snprintf(num_str, sizeof(num_str), "-%u", i);
		md5_update(&md5_ctx2, num_str, strlen(num_str));
		md5_final(&md5_ctx2, md5);

		vhost = array_append_space(&list->vhosts);
		vhost->host = host;
		for (j = 0; j < sizeof(vhost->hash); j++)
			vhost->hash = (vhost->hash << CHAR_BIT) | md5[j];
	}
}

static void mail_hosts_sort_ring(struct mail_host_list *list)
{
	struct mail_host *const *hostp;

	/* rebuild vhosts */
	array_clear(&list->vhosts);
	array_foreach(&list->hosts, hostp)
		mail_vhost_add(list, *hostp);
	array_sort(&list->vhosts, mail_vhost_cmp);
	list->hosts_unsorted = FALSE;
}

static void mail_hosts_sort_direct(struct mail_host_list *list)
{
	struct mail_vhost *vhost;
	struct mail_host *const *hostp;
	unsigned int i;

	array_sort(&list->hosts, mail_host_cmp);

	/* rebuild vhosts */
	array_clear(&list->vhosts);
	array_foreach(&list->hosts, hostp) {
		for (i = 0; i < (*hostp)->vhost_count; i++) {
			vhost = array_append_space(&list->vhosts);
			vhost->host = *hostp;
		}
	}
	list->hosts_unsorted = FALSE;
}

static void mail_hosts_sort(struct mail_host_list *list)
{
	if (list->consistent_hashing)
		mail_hosts_sort_ring(list);
	else
		mail_hosts_sort_direct(list);
}

struct mail_host *
mail_host_add_ip(struct mail_host_list *list, const struct ip_addr *ip,
		 const char *tag)
{
	struct mail_host *host;

	i_assert(tag != NULL);

	host = i_new(struct mail_host, 1);
	host->vhost_count = VHOST_MULTIPLIER;
	host->ip = *ip;
	host->tag = i_strdup(tag);
	array_append(&list->hosts, &host, 1);

	list->hosts_unsorted = TRUE;
	return host;
}

static int
mail_host_add(struct mail_host_list *list, const char *host, const char *tag)
{
	struct ip_addr *ips;
	unsigned int i, ips_count;

	if (net_gethostbyname(host, &ips, &ips_count) < 0) {
		i_error("Unknown mail host: %s", host);
		return -1;
	}

	for (i = 0; i < ips_count; i++)
		(void)mail_host_add_ip(list, &ips[i], tag);
	return 0;
}

static int
mail_hosts_add_range(struct mail_host_list *list,
		     struct ip_addr ip1, struct ip_addr ip2, const char *tag)
{
	uint32_t *ip1_arr, *ip2_arr;
	uint32_t i1, i2;
	unsigned int i, j, max_bits, last_bits;

	if (ip1.family != ip2.family) {
		i_error("IP address family mismatch: %s vs %s",
			net_ip2addr(&ip1), net_ip2addr(&ip2));
		return -1;
	}
	if (net_ip_cmp(&ip1, &ip2) > 0) {
		i_error("IP addresses reversed: %s-%s",
			net_ip2addr(&ip1), net_ip2addr(&ip2));
		return -1;
	}
	if (IPADDR_IS_V4(&ip1)) {
		ip1_arr = &ip1.u.ip4.s_addr;
		ip2_arr = &ip2.u.ip4.s_addr;
		max_bits = 32;
		last_bits = 8;
	} else {
#ifndef HAVE_IPV6
		i_error("IPv6 not supported");
		return -1;
#else
		ip1_arr = (void *)&ip1.u.ip6;
		ip2_arr = (void *)&ip2.u.ip6;
		max_bits = 128;
		last_bits = 16;
#endif
	}

	/* make sure initial bits match */
	for (i = 0; i < (max_bits-last_bits)/32; i++) {
		if (ip1_arr[i] != ip2_arr[i]) {
			i_error("IP address range too large: %s-%s",
				net_ip2addr(&ip1), net_ip2addr(&ip2));
			return -1;
		}
	}
	i1 = htonl(ip1_arr[i]);
	i2 = htonl(ip2_arr[i]);

	for (j = last_bits; j < 32; j++) {
		if ((i1 & (1U << j)) != (i2 & (1U << j))) {
			i_error("IP address range too large: %s-%s",
				net_ip2addr(&ip1), net_ip2addr(&ip2));
			return -1;
		}
	}

	/* create hosts from the final bits */
	do {
		ip1_arr[i] = ntohl(i1);
		(void)mail_host_add_ip(list, &ip1, tag);
		i1++;
	} while (ip1_arr[i] != ip2_arr[i]);
	return 0;
}

int mail_hosts_parse_and_add(struct mail_host_list *list,
			     const char *hosts_string)
{
	int ret = 0;

	T_BEGIN {
		const char *const *tmp, *p, *host1, *host2;
		struct ip_addr ip1, ip2;

		tmp = t_strsplit_spaces(hosts_string, " ");
		for (; *tmp != NULL; tmp++) {
			const char *tag, *value = *tmp;

			p = strchr(value, '@');
			if (p == NULL)
				tag = "";
			else {
				value = t_strdup_until(value, p++);
				tag = p;
			}
			p = strchr(value, '-');
			if (p != NULL) {
				/* see if this is ip1-ip2 range */
				host1 = t_strdup_until(value, p);
				host2 = p + 1;
				if (net_addr2ip(host1, &ip1) == 0 &&
				    net_addr2ip(host2, &ip2) == 0) {
					if (mail_hosts_add_range(list, ip1, ip2,
								 tag) < 0)
						ret = -1;
					continue;
				}
			}

			if (mail_host_add(list, value, tag) < 0)
				ret = -1;
		}
	} T_END;

	if (array_count(&list->hosts) == 0) {
		if (ret < 0)
			i_error("No valid servers specified");
		else
			i_error("Empty server list");
		ret = -1;
	}
	return ret;
}

void mail_host_set_tag(struct mail_host *host, const char *tag)
{
	i_assert(tag != NULL);

	i_free(host->tag);
	host->tag = i_strdup(tag);
}

void mail_host_set_vhost_count(struct mail_host_list *list,
			       struct mail_host *host, unsigned int vhost_count)
{
	host->vhost_count = vhost_count;
	list->hosts_unsorted = TRUE;
}

static void mail_host_free(struct mail_host *host)
{
	i_free(host->tag);
	i_free(host);
}

void mail_host_remove(struct mail_host_list *list, struct mail_host *host)
{
	struct mail_host *const *hosts;
	unsigned int i, count;

	hosts = array_get(&list->hosts, &count);
	for (i = 0; i < count; i++) {
		if (hosts[i] == host) {
			array_delete(&list->hosts, i, 1);
			break;
		}
	}

	mail_host_free(host);
	list->hosts_unsorted = TRUE;
}

struct mail_host *
mail_host_lookup(struct mail_host_list *list, const struct ip_addr *ip)
{
	struct mail_host *const *hostp;

	if (list->hosts_unsorted)
		mail_hosts_sort(list);

	array_foreach(&list->hosts, hostp) {
		if (net_ip_compare(&(*hostp)->ip, ip))
			return *hostp;
	}
	return NULL;
}

static struct mail_host *
mail_host_get_by_hash_ring(struct mail_host_list *list, unsigned int hash,
			   const char *tag)
{
	struct mail_host *host;
	const struct mail_vhost *vhosts;
	unsigned int i, count, idx;

	vhosts = array_get(&list->vhosts, &count);
	array_bsearch_insert_pos(&list->vhosts, &hash,
				 mail_vhost_hash_cmp, &idx);
	i_assert(idx <= count);
	if (idx == count) {
		if (count == 0)
			return NULL;
		idx = 0;
	}

	for (i = 0; i < count; i++) {
		host = vhosts[(idx + i) % count].host;
		if (strcmp(host->tag, tag) == 0)
			return host;
	}
	return NULL;
}

static struct mail_host *
mail_host_get_by_hash_direct(struct mail_host_list *list, unsigned int hash,
			     const char *tag)
{
	struct mail_host *host;
	const struct mail_vhost *vhosts;
	unsigned int i, count;

	vhosts = array_get(&list->vhosts, &count);
	if (count == 0)
		return NULL;

	for (i = 0; i < count; i++) {
		host = vhosts[(hash + i) % count].host;
		if (strcmp(host->tag, tag) == 0)
			return host;
	}
	return NULL;
}

struct mail_host *
mail_host_get_by_hash(struct mail_host_list *list, unsigned int hash,
		      const char *tag)
{
	if (list->hosts_unsorted)
		mail_hosts_sort(list);

	if (list->consistent_hashing)
		return mail_host_get_by_hash_ring(list, hash, tag);
	else
		return mail_host_get_by_hash_direct(list, hash, tag);
}

bool mail_hosts_have_usable(struct mail_host_list *list)
{
	if (list->hosts_unsorted)
		mail_hosts_sort(list);
	return array_count(&list->vhosts) > 0;
}

const ARRAY_TYPE(mail_host) *mail_hosts_get(struct mail_host_list *list)
{
	if (list->hosts_unsorted)
		mail_hosts_sort(list);
	return &list->hosts;
}

struct mail_host_list *mail_hosts_init(bool consistent_hashing)
{
	struct mail_host_list *list;

	list = i_new(struct mail_host_list, 1);
	list->consistent_hashing = consistent_hashing;
	i_array_init(&list->hosts, 16);
	i_array_init(&list->vhosts, 16*VHOST_MULTIPLIER);
	return list;
}

void mail_hosts_deinit(struct mail_host_list **_list)
{
	struct mail_host_list *list = *_list;
	struct mail_host **hostp;

	*_list = NULL;

	array_foreach_modifiable(&list->hosts, hostp)
		mail_host_free(*hostp);
	array_free(&list->hosts);
	array_free(&list->vhosts);
	i_free(list);
}

static struct mail_host *mail_host_dup(const struct mail_host *src)
{
	struct mail_host *dest;

	dest = i_new(struct mail_host, 1);
	*dest = *src;
	dest->tag = i_strdup(src->tag);
	return dest;
}

struct mail_host_list *mail_hosts_dup(const struct mail_host_list *src)
{
	struct mail_host_list *dest;
	struct mail_host *const *hostp, *dest_host;

	dest = mail_hosts_init(src->consistent_hashing);
	array_foreach(&src->hosts, hostp) {
		dest_host = mail_host_dup(*hostp);
		array_append(&dest->hosts, &dest_host, 1);
	}
	mail_hosts_sort(dest);
	return dest;
}