/* Copyright (c) Dovecot authors, see top-level COPYING file */

#define _GNU_SOURCE /* For Linux's struct ucred */
#include "lib.h"
#include "time-util.h"
#include "net.h"

#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/un.h>
#include <netinet/tcp.h>
#if defined(HAVE_UCRED_H)
#  include <ucred.h> /* for getpeerucred() */
#elif defined(HAVE_SYS_UCRED_H)
#  include <sys/ucred.h> /* for FreeBSD struct xucred */
#endif

union sockaddr_union {
	struct sockaddr sa;
	struct sockaddr_in sin;
	struct sockaddr_in6 sin6;
};

union sockaddr_union_unix {
	struct sockaddr sa;
	struct sockaddr_un un;
};

#define SIZEOF_SOCKADDR(so) ((so).sa.sa_family == AF_INET6 ? \
	sizeof(so.sin6) : sizeof(so.sin))

#if !defined(HAVE_GETPEEREID) && !defined(SO_PEERCRED) && !defined(HAVE_GETPEERUCRED) && defined(MSG_WAITALL) && defined(LOCAL_CREDS)
#  define NEEDS_LOCAL_CREDS 1
#else
#  undef NEEDS_LOCAL_CREDS
#endif

/* If connect() fails with EADDRNOTAVAIL (or some others on FreeBSD), retry it
   this many times.

   This is needed on busy systems kernel may assign the same source port to two
   sockets at bind() stage, which is what we generally want to allow more than
   64k outgoing connections to different destinations. However, at bind() stage
   the kernel doesn't know the destination yet. So it's possible that it
   assigns the same source port to two (or more) sockets that have the same
   destination IP+port as well. In this case connect() will fail with
   EADDRNOTAVAIL. We'll need to retry this and hope that the next attempt won't
   conflict. */
#define MAX_CONNECT_RETRIES 20

static int net_handle_gai_error(const char *function,
				int gai_errno, bool log)
{
	if (gai_errno == 0)
		return 0;
	switch (gai_errno) {
	/* errors that are not logged */
#ifdef EAI_ADDRFAMILY
	case EAI_ADDRFAMILY:
#endif
	case EAI_FAMILY:
#ifdef EAI_NODATA
	case EAI_NODATA:
#endif
	case EAI_NONAME:
		break;
	case EAI_MEMORY:
		i_fatal_status(FATAL_OUTOFMEM, "%s() failed: %s",
			       function, gai_strerror(gai_errno));
	case EAI_SYSTEM:
		if (log)
			i_error("%s() failed: %m", function);
		break;
	default:
		if (log)
			i_error("%s() failed: %s", function,
				gai_strerror(gai_errno));
		break;
	}
	return gai_errno;
}

bool net_ip_compare(const struct ip_addr *ip1, const struct ip_addr *ip2)
{
	return net_ip_cmp(ip1, ip2) == 0;
}

int net_ip_cmp(const struct ip_addr *ip1, const struct ip_addr *ip2)
{
	if (ip1->family != ip2->family)
		return ip1->family - ip2->family;

	switch (ip1->family) {
	case AF_INET6:
		return memcmp(&ip1->u.ip6, &ip2->u.ip6, sizeof(ip1->u.ip6));
	case AF_INET:
		return memcmp(&ip1->u.ip4, &ip2->u.ip4, sizeof(ip1->u.ip4));
	default:
		break;
	}
	return 0;
}

unsigned int net_ip_hash(const struct ip_addr *ip)
{
	const unsigned char *p;
	unsigned int len, g, h = 0;

	if (ip->family == AF_INET6) {
		p = ip->u.ip6.s6_addr;
		len = sizeof(ip->u.ip6);
	} else
	{
		return ip->u.ip4.s_addr;
	}

	for (; len > 0; len--, p++) {
		h = (h << 4) + *p;
		if ((g = h & 0xf0000000UL) != 0) {
			h = h ^ (g >> 24);
			h = h ^ g;
		}
	}

	return h;
}

/* copy IP to sockaddr */
static inline void
sin_set_ip(union sockaddr_union *so, const struct ip_addr *ip)
{
	if (ip == NULL) {
		so->sin6.sin6_family = AF_INET6;
		so->sin6.sin6_addr = in6addr_any;
		return;
	}

	so->sin.sin_family = ip->family;
	so->sin6.sin6_scope_id = ip->scope_id;
	if (ip->family == AF_INET6)
		memcpy(&so->sin6.sin6_addr, &ip->u.ip6, sizeof(ip->u.ip6));
	else
		memcpy(&so->sin.sin_addr, &ip->u.ip4, sizeof(ip->u.ip4));
}

static inline void
sin_get_ip(const union sockaddr_union *so, struct ip_addr *ip)
{
	/* IP structs may be sent across processes. Clear the whole struct
	   first to make sure it won't leak any data across processes. */
	i_zero(ip);

	ip->family = so->sin.sin_family;

	if (ip->family == AF_INET6) {
		memcpy(&ip->u.ip6, &so->sin6.sin6_addr, sizeof(ip->u.ip6));
		ip->scope_id = so->sin6.sin6_scope_id;
	} else if (ip->family == AF_INET)
		memcpy(&ip->u.ip4, &so->sin.sin_addr, sizeof(ip->u.ip4));
	else
		i_zero(&ip->u);
}

static inline void sin_set_port(union sockaddr_union *so, in_port_t port)
{
	if (so->sin.sin_family == AF_INET6)
		so->sin6.sin6_port = htons(port);
	else if (so->sin.sin_family == AF_INET)
		so->sin.sin_port = htons(port);
	else
		i_unreached();
}

static inline in_port_t sin_get_port(const union sockaddr_union *so)
{
	if (so->sin.sin_family == AF_INET6)
		return ntohs(so->sin6.sin6_port);
	if (so->sin.sin_family == AF_INET)
		return ntohs(so->sin.sin_port);

	return 0;
}

static int net_connect_ip_once(const struct ip_addr *ip, in_port_t port,
			       const struct ip_addr *my_ip, int sock_type, bool blocking)
{
	union sockaddr_union so;
	int fd, ret, opt = 1;

	if (my_ip != NULL && ip->family != my_ip->family) {
		i_warning("net_connect_ip(): ip->family != my_ip->family");
		my_ip = NULL;
	}

	/* create the socket */
	i_zero(&so);
	so.sin.sin_family = ip->family;
	fd = socket(ip->family, sock_type, 0);

	if (fd == -1) {
		i_error("socket() failed: %m");
		return -1;
	}

	/* set socket options */
	(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	if (sock_type == SOCK_STREAM)
		(void)setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
	if (!blocking)
		net_set_nonblock(fd, TRUE);

	/* set our own address */
	if (my_ip != NULL) {
		sin_set_ip(&so, my_ip);
		if (bind(fd, &so.sa, SIZEOF_SOCKADDR(so)) == -1) {
			i_error("bind(%s) failed: %m", net_ip2addr(my_ip));
			i_close_fd(&fd);
			return -1;
		}
	}

	/* connect */
	sin_set_ip(&so, ip);
	sin_set_port(&so, port);
	ret = connect(fd, &so.sa, SIZEOF_SOCKADDR(so));

#ifndef WIN32
	if (ret < 0 && errno != EINPROGRESS)
#else
	if (ret < 0 && WSAGetLastError() != WSAEWOULDBLOCK)
#endif
	{
		i_close_fd(&fd);
		return -1;
	}

	return fd;
}

static int net_connect_ip_full(const struct ip_addr *ip, in_port_t port,
			       const struct ip_addr *my_ip, int sock_type,
			       bool blocking)
{
	int fd, try;

	for (try = 0;;) {
		fd = net_connect_ip_once(ip, port, my_ip, sock_type, blocking);
		if (fd != -1 || try++ >= MAX_CONNECT_RETRIES ||
		    (errno != EADDRNOTAVAIL
#if defined(__FreeBSD__) || defined(__OpenBSD__)
		     /* busy */
		     && errno != EADDRINUSE
		     /* pf may cause this if another connection used
			the same port recently */
		     && errno != EACCES
#endif
		    ))
			break;
	}
	return fd;
}

int net_connect_ip(const struct ip_addr *ip, in_port_t port,
		   const struct ip_addr *my_ip)
{
	return net_connect_ip_full(ip, port, my_ip, SOCK_STREAM, FALSE);
}

int net_connect_ip_blocking(const struct ip_addr *ip, in_port_t port,
			    const struct ip_addr *my_ip)
{
	return net_connect_ip_full(ip, port, my_ip, SOCK_STREAM, TRUE);
}

int net_connect_udp(const struct ip_addr *ip, in_port_t port,
			       const struct ip_addr *my_ip)
{
	return net_connect_ip_full(ip, port, my_ip, SOCK_DGRAM, FALSE);
}

int net_try_bind(const struct ip_addr *ip)
{
	union sockaddr_union so;
	int fd;

	/* create the socket */
	i_zero(&so);
	so.sin.sin_family = ip->family;
	fd = socket(ip->family, SOCK_STREAM, 0);
	if (fd == -1) {
		i_error("socket() failed: %m");
		return -1;
	}

	sin_set_ip(&so, ip);
	if (bind(fd, &so.sa, SIZEOF_SOCKADDR(so)) == -1) {
		i_close_fd(&fd);
		return -1;
	}
	i_close_fd(&fd);
	return 0;
}

int net_connect_unix(const char *path)
{
	union sockaddr_union_unix sa;
	int fd, ret;

	i_zero(&sa);
	sa.un.sun_family = AF_UNIX;
	if (i_strocpy(sa.un.sun_path, path, sizeof(sa.un.sun_path)) < 0) {
		/* too long path */
#ifdef ENAMETOOLONG
		errno = ENAMETOOLONG;
#else
		errno = EOVERFLOW;
#endif
		return -1;
	}

	/* create the socket */
	fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd == -1) {
		i_error("socket(%s) failed: %m", path);
		return -1;
	}

	net_set_nonblock(fd, TRUE);

	/* connect */
	ret = connect(fd, &sa.sa, sizeof(sa));
	if (ret < 0 && errno != EINPROGRESS) {
		i_close_fd(&fd);
		return -1;
	}

#ifdef NEEDS_LOCAL_CREDS
	{
		int on = 1;
		if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof on)) {
			i_error("setsockopt(LOCAL_CREDS) failed: %m");
			return -1;
		}
	}
#endif

	return fd;
}

int net_connect_unix_with_retries(const char *path, unsigned int msecs)
{
	struct timeval start, now;
	int fd;

	i_gettimeofday(&start);

	do {
		fd = net_connect_unix(path);
		if (fd != -1 || (errno != EAGAIN && errno != ECONNREFUSED))
			break;

		/* busy. wait for a while. */
		usleep(i_rand_minmax(1, 10) * 10000);
		i_gettimeofday(&now);
	} while (timeval_diff_msecs(&now, &start) < (int)msecs);
	return fd;
}

void net_disconnect(int fd)
{
	/* FreeBSD's close() fails with ECONNRESET if socket still has unsent
	   data in transmit buffer. We don't care. */
	if (close(fd) < 0 && errno != ECONNRESET)
		i_error("net_disconnect() failed: %m");
}

void net_set_nonblock(int fd, bool nonblock)
{
	fd_set_nonblock(fd, nonblock);
}

int net_set_cork(int fd ATTR_UNUSED, bool cork ATTR_UNUSED)
{
#ifdef TCP_CORK
	int val = cork;

	return setsockopt(fd, IPPROTO_TCP, TCP_CORK, &val, sizeof(val));
#else
	errno = ENOPROTOOPT;
	return -1;
#endif
}

int net_set_tcp_nodelay(int fd, bool nodelay)
{
	int val = nodelay;

	return setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val));
}

int net_set_tcp_quickack(int fd ATTR_UNUSED, bool quickack ATTR_UNUSED)
{
#ifdef TCP_QUICKACK
	int val = quickack;

	return setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &val, sizeof(val));
#else
	errno = ENOPROTOOPT;
	return -1;
#endif
}

int net_set_send_buffer_size(int fd, size_t size)
{
	int opt;

	if (size > INT_MAX) {
		errno = EINVAL;
		return -1;
	}
	opt = (int)size;
	return setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt));
}

int net_set_recv_buffer_size(int fd, size_t size)
{
	int opt;

	if (size > INT_MAX) {
		errno = EINVAL;
		return -1;
	}
	opt = (int)size;
	return setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(opt));
}

const struct ip_addr net_ip4_any = {
	.family = AF_INET,
	.u.ip4.s_addr = INADDR_ANY
};

const struct ip_addr net_ip6_any = {
	.family = AF_INET6,
	.u.ip6 = IN6ADDR_ANY_INIT
};

const struct ip_addr net_ip4_loopback = {
	.family = AF_INET,
	.u.ip4.s_addr = INADDR_LOOPBACK
};

const struct ip_addr net_ip6_loopback = {
	.family = AF_INET6,
	.u.ip6 = IN6ADDR_LOOPBACK_INIT
};

int net_listen(const struct ip_addr *my_ip, in_port_t *port, int backlog)
{
	enum net_listen_flags flags = 0;

	return net_listen_full(my_ip, port, &flags, backlog);
}

int net_listen_full(const struct ip_addr *my_ip, in_port_t *port,
		    enum net_listen_flags *flags, int backlog)
{
	union sockaddr_union so;
	int ret, fd, opt = 1;
	socklen_t len;

	i_zero(&so);
	sin_set_ip(&so, my_ip);
	sin_set_port(&so, *port);

	/* create the socket */
	fd = socket(so.sin.sin_family, SOCK_STREAM, 0);
	if (fd == -1 && my_ip == NULL &&
	    (errno == EINVAL || errno == EAFNOSUPPORT)) {
		/* IPv6 is not supported by OS */
		so.sin.sin_family = AF_INET;
		so.sin.sin_addr.s_addr = INADDR_ANY;

		fd = socket(AF_INET, SOCK_STREAM, 0);
	}
	if (fd == -1) {
		i_error("socket() failed: %m");
		return -1;
	}

	/* set socket options */
	(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	(void)setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));

	if ((*flags & NET_LISTEN_FLAG_REUSEPORT) != 0) {
#ifdef SO_REUSEPORT
		if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT,
			       &opt, sizeof(opt)) < 0)
#endif
			*flags &= ENUM_NEGATE(NET_LISTEN_FLAG_REUSEPORT);
	}

	/* If using IPv6, bind only to IPv6 if possible. This avoids
	   ambiguities with IPv4-mapped IPv6 addresses. */
#ifdef IPV6_V6ONLY
	if (so.sin.sin_family == AF_INET6) {
		opt = 1;
		(void)setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
	}
#endif
	/* specify the address/port we want to listen in */
	ret = bind(fd, &so.sa, SIZEOF_SOCKADDR(so));
	if (ret < 0) {
		if (errno != EADDRINUSE) {
			i_error("bind(%s, %u) failed: %m",
				my_ip == NULL ? "" : net_ip2addr(my_ip), *port);
		}
	} else {
		/* get the actual port we started listen */
		len = SIZEOF_SOCKADDR(so);
		ret = getsockname(fd, &so.sa, &len);
		if (ret >= 0) {
			*port = sin_get_port(&so);

			/* start listening */
			if (listen(fd, backlog) >= 0)
				return fd;

			if (errno != EADDRINUSE)
				i_error("listen() failed: %m");
		}
	}

	/* error */
	i_close_fd(&fd);
	return -1;
}

int net_listen_unix(const char *path, int backlog)
{
	union {
		struct sockaddr sa;
		struct sockaddr_un un;
	} sa;
	int fd;

	i_zero(&sa);
	sa.un.sun_family = AF_UNIX;
	if (i_strocpy(sa.un.sun_path, path, sizeof(sa.un.sun_path)) < 0) {
		/* too long path */
#ifdef ENAMETOOLONG
		errno = ENAMETOOLONG;
#else
		errno = EOVERFLOW;
#endif
		return -1;
	}

	/* create the socket */
	fd = socket(PF_UNIX, SOCK_STREAM, 0);
	if (fd == -1) {
		i_error("socket() failed: %m");
		return -1;
	}

#ifdef NEEDS_LOCAL_CREDS
	{
		int on = 1;
		if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof on)) {
			i_error("setsockopt(LOCAL_CREDS) failed: %m");
			return -1;
		}
	}
#endif

	/* bind */
	if (bind(fd, &sa.sa, sizeof(sa)) < 0) {
		if (errno != EADDRINUSE)
			i_error("bind(%s) failed: %m", path);
	} else {
		/* start listening */
		if (listen(fd, backlog) == 0)
			return fd;

		if (errno != EADDRINUSE)
			i_error("listen() failed: %m");
	}

	i_close_fd(&fd);
	return -1;
}

int net_listen_unix_unlink_stale(const char *path, int backlog)
{
	unsigned int i = 0;
	int fd;

	while ((fd = net_listen_unix(path, backlog)) == -1) {
		if (errno != EADDRINUSE || ++i == 2)
			return -1;

		/* see if it really exists */
		fd = net_connect_unix(path);
		if (fd != -1 || errno != ECONNREFUSED) {
			i_close_fd(&fd);
			errno = EADDRINUSE;
			return -1;
		}

		/* delete and try again */
		if (i_unlink_if_exists(path) < 0) {
			errno = EADDRINUSE;
			return -1;
		}
	}
	return fd;
}

int net_accept(int fd, struct ip_addr *addr_r, in_port_t *port_r)
{
	union sockaddr_union so;
	int ret;
	socklen_t addrlen;

	i_assert(fd >= 0);

	i_zero(&so);
	addrlen = sizeof(so);
	ret = accept(fd, &so.sa, &addrlen);

	if (ret < 0)
		return -1;
	if (so.sin.sin_family == AF_UNIX) {
		if (addr_r != NULL)
			i_zero(addr_r);
		if (port_r != NULL) *port_r = 0;
	} else {
		if (addr_r != NULL) sin_get_ip(&so, addr_r);
		if (port_r != NULL) *port_r = sin_get_port(&so);
	}
	return ret;
}

ssize_t net_receive(int fd, void *buf, size_t len)
{
	ssize_t ret;

	i_assert(fd >= 0);
	i_assert(len <= SSIZE_T_MAX);

	ret = read(fd, buf, len);
	if (ret == 0) {
		/* disconnected */
		errno = 0;
		return -2;
	}

	if (unlikely(ret < 0)) {
		if (errno == EINTR || errno == EAGAIN)
			return 0;

		if (errno == ECONNRESET || errno == ETIMEDOUT) {
			/* treat as disconnection */
			return -2;
		}
	}

	return ret;
}

int net_gethostbyname(const char *addr, struct ip_addr **ips,
		      unsigned int *ips_count)
{
	/* @UNSAFE */
	const union sockaddr_union *so;
	struct addrinfo hints, *ai, *origai;
	struct ip_addr ip;
	int host_error;
	int count;

	*ips = NULL;
	*ips_count = 0;

	/* support [ipv6] style addresses here so they work globally */
	if (addr[0] == '[' && net_addr2ip(addr, &ip) == 0) {
		*ips_count = 1;
		*ips = t_new(struct ip_addr, 1);
		**ips = ip;
		return 0;
	}

	i_zero(&hints);
	hints.ai_socktype = SOCK_STREAM;

	ai = NULL;
	/* save error to host_error for later use */
	host_error = getaddrinfo(addr, NULL, &hints, &ai);
	if (net_handle_gai_error("getaddrinfo", host_error, FALSE) != 0) {
		i_assert(ai == NULL);
		return host_error;
	}

	/* get number of IPs */
	origai = ai;
	for (count = 0; ai != NULL; ai = ai->ai_next)
		count++;
	i_assert(count > 0);

	*ips_count = count;
	*ips = t_new(struct ip_addr, count);

	count = 0;
	for (ai = origai; ai != NULL; ai = ai->ai_next, count++) {
		so = (union sockaddr_union *) ai->ai_addr;

		sin_get_ip(so, &(*ips)[count]);
	}
	freeaddrinfo(origai);

	return 0;
}

int net_gethostbyaddr(const struct ip_addr *ip, const char **name_r)
{
	union sockaddr_union so;
	socklen_t addrlen = sizeof(so);
	char hbuf[NI_MAXHOST];
	int ret;

	i_zero(&so);
	sin_set_ip(&so, ip);
	ret = getnameinfo(&so.sa, addrlen, hbuf, sizeof(hbuf), NULL, 0,
			  NI_NAMEREQD);
	if (net_handle_gai_error("getnameinfo", ret, FALSE) != 0)
		return ret;

	*name_r = t_strdup(hbuf);
	return 0;
}

int net_getsockname(int fd, struct ip_addr *addr, in_port_t *port)
{
	union sockaddr_union so;
	socklen_t addrlen;

	i_assert(fd >= 0);

	i_zero(&so);
	addrlen = sizeof(so);
	if (getsockname(fd, &so.sa, &addrlen) == -1)
		return -1;
	if (so.sin.sin_family == AF_UNIX) {
		if (addr != NULL)
			i_zero(addr);
		if (port != NULL) *port = 0;
	} else {
		if (addr != NULL) sin_get_ip(&so, addr);
		if (port != NULL) *port = sin_get_port(&so);
	}
	return 0;
}

int net_getpeername(int fd, struct ip_addr *addr, in_port_t *port)
{
	union sockaddr_union so;
	socklen_t addrlen;

	i_assert(fd >= 0);

	i_zero(&so);
	addrlen = sizeof(so);
	if (getpeername(fd, &so.sa, &addrlen) == -1)
		return -1;
	if (so.sin.sin_family == AF_UNIX) {
		if (addr != NULL)
			i_zero(addr);
		if (port != NULL) *port = 0;
	} else {
		if (addr != NULL) sin_get_ip(&so, addr);
		if (port != NULL) *port = sin_get_port(&so);
	}
	return 0;
}

int net_getunixname(int fd, const char **name_r)
{
	union sockaddr_union_unix so;
	socklen_t addrlen = sizeof(so);

	i_zero(&so);
	if (getsockname(fd, &so.sa, &addrlen) < 0)
		return -1;
	if (so.un.sun_family != AF_UNIX) {
		errno = ENOTSOCK;
		return -1;
	}
	*name_r = t_strdup(so.un.sun_path);
	return 0;
}

int net_getunixcred(int fd, struct net_unix_cred *cred_r)
{
#if defined(SO_PEERCRED)
# if defined(HAVE_STRUCT_SOCKPEERCRED)
	/* OpenBSD (may also provide getpeereid, but we also want pid) */
	struct sockpeercred ucred;
# else
	/* Linux */
	struct ucred ucred;
# endif
	socklen_t len = sizeof(ucred);

	if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) < 0) {
		i_error("getsockopt(SO_PEERCRED) failed: %m");
		return -1;
	}
	cred_r->uid = ucred.uid;
	cred_r->gid = ucred.gid;
	cred_r->pid = ucred.pid;
	return 0;
#elif defined(LOCAL_PEEREID)
	/* NetBSD (may also provide getpeereid, but we also want pid) */
	struct unpcbid ucred;
	socklen_t len = sizeof(ucred);

	if (getsockopt(fd, 0, LOCAL_PEEREID, &ucred, &len) < 0) {
		i_error("getsockopt(LOCAL_PEEREID) failed: %m");
		return -1;
	}

	cred_r->uid = ucred.unp_euid;
	cred_r->gid = ucred.unp_egid;
	cred_r->pid = ucred.unp_pid;
	return 0;
#elif defined(HAVE_GETPEEREID)
	/* OSX 10.4+, FreeBSD 4.6+, OpenBSD 3.0+, NetBSD 5.0+ */
	if (getpeereid(fd, &cred_r->uid, &cred_r->gid) < 0) {
		i_error("getpeereid() failed: %m");
		return -1;
	}
	cred_r->pid = (pid_t)-1;
	return 0;
#elif defined(LOCAL_PEERCRED)
	/* Older FreeBSD */
	struct xucred ucred;
	socklen_t len = sizeof(ucred);

	if (getsockopt(fd, 0, LOCAL_PEERCRED, &ucred, &len) < 0) {
		i_error("getsockopt(LOCAL_PEERCRED) failed: %m");
		return -1;
	}

	if (ucred.cr_version != XUCRED_VERSION) {
		errno = EINVAL;
		return -1;
	}

	cred_r->uid = ucred.cr_uid;
	cred_r->gid = ucred.cr_gid;
	cred_r->pid = (pid_t)-1;
	return 0;
#elif defined(HAVE_GETPEERUCRED)
	/* Solaris */
	ucred_t *ucred = NULL;

	if (getpeerucred(fd, &ucred) < 0) {
		i_error("getpeerucred() failed: %m");
		return -1;
	}
	cred_r->uid = ucred_geteuid(ucred);
	cred_r->gid = ucred_getrgid(ucred);
	cred_r->pid = ucred_getpid(ucred);
	ucred_free(ucred);

	if (cred_r->uid == (uid_t)-1 ||
	    cred_r->gid == (gid_t)-1) {
		errno = EINVAL;
		return -1;
	}
	return 0;
#elif defined(NEEDS_LOCAL_CREDS)
	/* NetBSD < 5 */
	int i, n, on;
	struct iovec iov;
	struct msghdr msg;
	struct {
		struct cmsghdr ch;
		char buf[110];
	} cdata;
	struct sockcred *sc;

	iov.iov_base = (char *)&on;
	iov.iov_len = 1;

	sc = (struct sockcred *)cdata.buf;
	sc->sc_uid = sc->sc_euid = sc->sc_gid = sc->sc_egid = -1;
	i_zero(&cdata.ch);

	i_zero(&msg);

	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = &cdata;
	msg.msg_controllen = sizeof(cdata.ch) + sizeof(cdata.buf);

	for (i = 0; i < 10; i++) {
		n = recvmsg(fd, &msg, MSG_WAITALL | MSG_PEEK);
		if (n >= 0 || errno != EAGAIN)
			break;
		usleep(100);
	}
	if (n < 0) {
		i_error("recvmsg() failed: %m");
		return -1;
	}
	cred_r->uid = sc->sc_euid;
	cred_r->gid = sc->sc_egid;
	cred_r->pid = (pid_t)-1;
	return 0;
#else
	errno = EINVAL;
	return -1;
#endif
}

const char *net_ip2addr(const struct ip_addr *ip)
{
	union sockaddr_union u;
	socklen_t so_len;

	if (ip->family == AF_INET) {
		u.sin.sin_addr = ip->u.ip4;
		u.sin.sin_family = ip->family;
		so_len = sizeof(u.sin);
	} else if (ip->family == AF_INET6) {
		u.sin6.sin6_addr = ip->u.ip6;
		u.sin6.sin6_family = ip->family;
		u.sin6.sin6_scope_id = ip->scope_id;
		so_len = sizeof(u.sin6);
	} else /* not an IP */
		return "";

	char *addr = t_malloc_no0(MAX_IP_LEN+1);
	int ret;
	if ((ret = getnameinfo(&u.sa, so_len, addr, MAX_IP_LEN+1, NULL, 0,
			       NI_NUMERICHOST)) < 0) {
		(void)net_handle_gai_error("getnameinfo", ret, TRUE);
		return "";
	}
	return addr;
}

static bool net_addr2ip_inet4_fast(const char *addr, struct ip_addr *ip)
{
	uint8_t *saddr = (void *)&ip->u.ip4.s_addr;
	unsigned int i, num;

	if (str_parse_uint(addr, &num, &addr) < 0)
		return FALSE;

	/* try to parse as a.b.c.d */
	i = 0;
	for (;;) {
		if (num >= 256)
			return FALSE;
		saddr[i] = num;
		if (i == 3)
			break;
		i++;
		if (*addr != '.')
			return FALSE;
		addr++;
		if (str_parse_uint(addr, &num, &addr) < 0)
			return FALSE;
	}
	if (*addr != '\0')
		return FALSE;
	ip->family = AF_INET;
	return TRUE;
}

int net_addr2ip(const char *addr, struct ip_addr *ip_r)
{
	int ret;

	if (net_addr2ip_inet4_fast(addr, ip_r))
		return 0;

	T_BEGIN {
		if (strchr(addr, ':') != NULL) {
			if (addr[0] == '[') {
				/* allow [ipv6 addr] */
				size_t len = strlen(addr);
				if (addr[len-1] == ']')
					addr = t_strndup(addr+1, len-2);
			}
		}

		struct addrinfo *res = NULL;
		const struct addrinfo hints = {
			.ai_flags = AI_NUMERICHOST,
		};
		if ((ret = getaddrinfo(addr, NULL, &hints, &res)) == 0) {
			i_assert(res != NULL);
			const union sockaddr_union *so =
				(union sockaddr_union *)res->ai_addr;
			sin_get_ip(so, ip_r);
		}
		if (res != NULL)
			freeaddrinfo(res);
		ret = net_handle_gai_error("getaddrinfo", ret, TRUE);
	} T_END;
	return ret != 0 ? -1 : 0;
}

int net_str2port(const char *str, in_port_t *port_r)
{
	uintmax_t l;

	if (str_to_uintmax(str, &l) < 0)
		return -1;

	if (l == 0 || l > (in_port_t)-1)
		return -1;
	*port_r = (in_port_t)l;
	return 0;
}

int net_str2port_zero(const char *str, in_port_t *port_r)
{
	uintmax_t l;

	if (str_to_uintmax(str, &l) < 0)
		return -1;

	if (l > (in_port_t)-1)
		return -1;
	*port_r = (in_port_t)l;
	return 0;
}

int net_str2hostport(const char *str, in_port_t default_port,
		     const char **host_r, in_port_t *port_r)
{
	const char *p, *host;
	in_port_t port;

	if (str[0] == '[') {
		/* [IPv6] address, possibly followed by :port */
		p = strchr(str, ']');
		if (p == NULL)
			return -1;
		host = t_strdup_until(str+1, p++);
	} else {
		p = strchr(str, ':');
		if (p == NULL || strchr(p+1, ':') != NULL) {
			/* host or IPv6 address */
			*host_r = str;
			*port_r = default_port;
			return 0;
		}
		host = t_strdup_until(str, p);
	}
	if (p[0] == '\0') {
		*host_r = host;
		*port_r = default_port;
		return 0;
	}
	if (p[0] != ':')
		return -1;
	if (net_str2port(p+1, &port) < 0)
		return -1;
	*host_r = host;
	*port_r = port;
	return 0;
}

const char *net_ipport2str(const struct ip_addr *ip, in_port_t port)
{
	i_assert(IPADDR_IS_V4(ip) || IPADDR_IS_V6(ip));

	return t_strdup_printf("%s%s%s:%u",
			       (IPADDR_IS_V6(ip) ? "[" : ""), net_ip2addr(ip),
			       (IPADDR_IS_V6(ip) ? "]" : ""), port);
}

int net_ipv6_mapped_ipv4_convert(const struct ip_addr *src,
				 struct ip_addr *dest)
{
	static const uint8_t v4_prefix[] =
		{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };

	if (!IPADDR_IS_V6(src))
		return -1;
	if (memcmp(src->u.ip6.s6_addr, v4_prefix, sizeof(v4_prefix)) != 0)
		return -1;

	i_zero(dest);
	dest->family = AF_INET;
	memcpy(&dest->u.ip6, &src->u.ip6.s6_addr[3*4], 4);
	return 0;
}

int net_geterror(int fd)
{
	int data;
	socklen_t len = sizeof(data);

	if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &data, &len) == -1) {
		/* we're now really returning the getsockopt()'s error code
		   instead of the socket's, but normally we should never get
		   here anyway. */
		return errno;
	}

	return data;
}

const char *net_gethosterror(int error)
{
	i_assert(error != 0);

	return gai_strerror(error);
}

enum net_hosterror_type net_get_hosterror_type(int error)
{
	const struct {
		int error;
		enum net_hosterror_type type;
	} error_map[] = {
#ifdef EAI_ADDRFAMILY /* Obsoleted by RFC 2553bis-02 */
		{ EAI_ADDRFAMILY, NET_HOSTERROR_TYPE_NOT_FOUND },
#endif
		{ EAI_AGAIN, NET_HOSTERROR_TYPE_NAMESERVER },
		{ EAI_BADFLAGS, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
		{ EAI_FAIL, NET_HOSTERROR_TYPE_NAMESERVER },
		{ EAI_FAMILY, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
		{ EAI_MEMORY, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
#ifdef EAI_NODATA /* Obsoleted by RFC 2553bis-02 */
		{ EAI_NODATA, NET_HOSTERROR_TYPE_NOT_FOUND },
#endif
		{ EAI_NONAME, NET_HOSTERROR_TYPE_NOT_FOUND },
		{ EAI_SERVICE, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
		{ EAI_SOCKTYPE, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
		{ EAI_SYSTEM, NET_HOSTERROR_TYPE_INTERNAL_ERROR },
	};
	for (unsigned int i = 0; i < N_ELEMENTS(error_map); i++) {
		if (error_map[i].error == error)
			return error_map[i].type;
	}

	/* shouldn't happen? assume internal error */
	return NET_HOSTERROR_TYPE_INTERNAL_ERROR;
}

int net_hosterror_notfound(int error)
{
#ifdef EAI_NODATA /* NODATA is deprecated */
	return (error != 1 && (error == EAI_NONAME || error == EAI_NODATA)) ? 1 : 0;
#else
	return (error != 1 && (error == EAI_NONAME)) ? 1 : 0;
#endif
}

const char *net_getservbyport(in_port_t port)
{
	const struct servent *entry;

	entry = getservbyport(htons(port), "tcp");
	return entry == NULL ? NULL : entry->s_name;
}

int net_parse_range(const char *network, struct ip_addr *ip_r,
		    unsigned int *bits_r)
{
	const char *p;
	unsigned int bits, max_bits;

	p = strchr(network, '/');
	if (p != NULL)
		network = t_strdup_until(network, p++);

	if (net_addr2ip(network, ip_r) < 0)
		return -1;

	max_bits = IPADDR_BITS(ip_r);
	if (p == NULL) {
		/* full IP address must match */
		bits = max_bits;
	} else {
		/* get the network mask */
		if (str_to_uint(p, &bits) < 0 || bits > max_bits)
			return -1;
	}
	*bits_r = bits;
	return 0;
}

bool net_is_in_network(const struct ip_addr *ip,
		       const struct ip_addr *net_ip, unsigned int bits)
{
	struct ip_addr tmp_ip;
	const uint32_t *ip1, *ip2;
	uint32_t mask, i1, i2;
	unsigned int pos, i;

	if (net_ipv6_mapped_ipv4_convert(ip, &tmp_ip) == 0) {
		/* IPv4 address mapped disguised as IPv6 address */
		ip = &tmp_ip;
	}

	if (ip->family == 0 || net_ip->family == 0) {
		/* non-IPv4/IPv6 address (e.g. UNIX socket) never matches
		   anything */
		return FALSE;
	}
	if (IPADDR_IS_V4(ip) != IPADDR_IS_V4(net_ip)) {
		/* one is IPv6 and one is IPv4 */
		return FALSE;
	}
	i_assert(IPADDR_IS_V6(ip) == IPADDR_IS_V6(net_ip));

	if (IPADDR_IS_V4(ip)) {
		ip1 = &ip->u.ip4.s_addr;
		ip2 = &net_ip->u.ip4.s_addr;
	} else {
		ip1 = (const void *)&ip->u.ip6;
		ip2 = (const void *)&net_ip->u.ip6;
		if (ip->scope_id != net_ip->scope_id &&
		    net_ip->scope_id != 0) {
			/* %iface1 != %iface2 never matches, but allow
			   missing interface on the net_ip */
			return FALSE;
		}
	}

	/* check first the full 32bit ints */
	for (pos = 0, i = 0; pos + 32 <= bits; pos += 32, i++) {
		if (ip1[i] != ip2[i])
			return FALSE;
	}
	i1 = htonl(ip1[i]);
	i2 = htonl(ip2[i]);

	/* check the last full bytes */
	for (mask = 0xff000000; pos + 8 <= bits; pos += 8, mask >>= 8) {
		if ((i1 & mask) != (i2 & mask))
			return FALSE;
	}

	/* check the last bits, they're reversed in bytes */
	bits -= pos;
	for (mask = 0x80000000 >> (pos % 32); bits > 0; bits--, mask >>= 1) {
		if ((i1 & mask) != (i2 & mask))
			return FALSE;
	}
	return TRUE;
}
