/* checkpeerlocal.c -- check if the peer address of a socket is on a
 *                     local network.
 * (C) 2002 by Matthias Andree <matthias.andree@gmx.de>
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of version 2 of the GNU Lesser General Public License as
 * published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library (look for the COPYING.LGPL file); if
 * not, write to the Free Software Foundation, Inc., 59 Temple Place,
 * Suite 330, Boston, MA 02111-1307 USA
 */

#include "config.h"
#include "mastring.h"
#include "critmem.h"
#include "leafnode.h"

#include <sys/types.h>
#include <sys/socket.h>
#ifdef HAVE_SYS_SOCKIO_H
/* needed for SunOS 5, IRIX, ... */
#include <sys/sockio.h>
#endif
#include <sys/ioctl.h>
#include <net/if.h>
#include <netinet/in.h>
#include <errno.h>
#ifndef __LCLINT__
#include <arpa/inet.h>
#endif /* not __LCLINT__ */
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "strlcpy.h"

#ifdef TEST
#define D(a) a
#else
#define D(a)
#endif

#ifdef TEST
static void pat(struct sockaddr *addr)
{
    char buf[512]; /* RATS: ignore */
    char *tag = "";
    switch (addr->sa_family) {
#ifdef HAVE_IPV6
	case AF_INET6:
	    inet_ntop(addr->sa_family,
		    &((struct sockaddr_in6 *)addr)->sin6_addr, buf, sizeof(buf));
	    tag = "IPv6: ";
	    break;
#endif
	case AF_INET:
	    strlcpy(buf, inet_ntoa(((struct sockaddr_in *)addr)->sin_addr),
		    sizeof(buf));
	    tag = "IPv4: ";
	    break;
	default:
	    strlcpy(buf, "unsupported address type", sizeof(buf));
	    break;
    }
    printf("%s%s\n", tag, buf);
}
#endif

/*
 * checks whether the peer of the socket is local to any of our network
 * interfaces, returns -1 for error (check errno), -2 for other error,
 * 0 for no, 1 for yes. If sock is not a socket, also returns 1.
 */
int checkpeerlocal(int sock)
{
    char addr[128]; /* RATS: ignore */
    mastr *buf;
    struct ifconf ifc;
    struct ifreq *ifr, *end;
    int type;
    socklen_t size;
    int newsock;

    /* obtain peer address */
    size = sizeof(addr);
#ifdef TEST_LOCAL
    sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (getsockname(sock, (struct sockaddr *)addr, &size)) {
#else
    if (getpeername(sock, (struct sockaddr *)addr, &size)) {
#endif
	if (errno == ENOTSOCK)
	    return 1;
	else
	    return -1;
    }

    type = ((struct sockaddr *)addr)->sa_family;
    D(printf("address type of peer socket: %d\n", type));

    switch(type) {
	case AF_INET:
	    break;
#ifdef HAVE_IPV6
	case AF_INET6:
	    break;
#endif
	default:
	    D(printf("address type not supported.\n"));
	    return -2;
    }

    D(pat((struct sockaddr *)addr));

#ifdef HAVE_IPV6
    if (type == AF_INET6) {
	struct sockaddr_in6 *i6 = (struct sockaddr_in6 *)addr;
	D((printf("IPv6 address\n")));
	/* IPv6 localhost */
	if (IN6_IS_ADDR_LOOPBACK(&i6->sin6_addr)) {
	    D((printf("IPv6 loopback address\n")));
	    return 1;
	} else if (IN6_IS_ADDR_LINKLOCAL(&i6->sin6_addr)) {
	    D((printf("IPv6 link local address\n")));
	    return 1;
	} else if (IN6_IS_ADDR_SITELOCAL(&i6->sin6_addr)) {
	    D((printf("IPv6 site local address\n")));
	    return 1;
	} else if (IN6_IS_ADDR_V4MAPPED(&i6->sin6_addr)) {
	    /* map to IPv4 */
	    struct sockaddr_in si;
	    D((printf("IPv4 mapped IPv6 address\n")));
	    si.sin_family = AF_INET;
	    si.sin_port = i6->sin6_port;
	    memcpy(&si.sin_addr, &(i6->sin6_addr.s6_addr[12]), 4);
	    memcpy(addr, &si, sizeof(struct sockaddr_in));
	} else {
	    return 0;
	}
    }
#endif

    D(pat((struct sockaddr *)addr));

    buf = mastr_new(2048);

    newsock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
	return -1;

    /* get list of address information */
    for (;;) {
	ifc.ifc_len = mastr_size(buf);
	ifc.ifc_buf = mastr_modifyable_str(buf);
	if (ioctl(newsock, SIOCGIFCONF, (char *)&ifc) < 0) {
	    if (errno != EINVAL) {
		close(sock);
		mastr_delete(buf);
		return -1;
	    }
	} 

	/* work around bugs in old Solaris (see Postfix'
	 * inet_addr_local.c for details) */
	if ((unsigned)ifc.ifc_len < mastr_size(buf) / 2) {
	    break;
	}

	mastr_resizekill(buf, mastr_size(buf) * 2);
    }

    /* get addresses and netmasks */
    end = (struct ifreq *)((char *)ifc.ifc_buf + ifc.ifc_len);
    for (ifr = ifc.ifc_req ; ifr < end ;
#ifdef HAVE_SALEN
	    ifr = ((struct ifreq *)
			    ((char *) ifr + sizeof(ifr->ifr_name)
			     + ifr->ifr_addr.sa_len))
#else
	    ifr++
#endif
	    )
    {
	    struct in_addr sia;
	    
#ifdef HAVE_SALEN
	    D(printf("interface: name %s, address type: %d, sa_len: %d\n", ifr->ifr_name,
			ifr->ifr_addr.sa_family, ifr->ifr_addr.sa_len));
#else
	    D(printf("interface: name %s, address type: %d\n", ifr->ifr_name,
			ifr->ifr_addr.sa_family));
#endif
	    sia = ((struct sockaddr_in *)&ifr->ifr_addr)->sin_addr;
	    switch (ifr->ifr_addr.sa_family) {
		case AF_INET:
		    break;
#ifdef HAVE_IPV6
		case AF_INET6:
		    break;
#endif
		default:
		    continue;
	    }
	    D(pat((struct sockaddr *)&ifr->ifr_addr));
	    if (sia.s_addr != INADDR_ANY) {
		struct in_addr adr, msk;
		struct ifreq ir;

		memcpy(&ir, ifr, sizeof(struct ifreq));
		/* Prevent nasty surprises on old Linux kernels with
		 * BSD-style IP aliasing (more than one IPv4 address on
		 * the same interface) -- SIOCGIFADDR/...NETMASK will
		 * return address and netmask for the first address,
		 * while SIOCGIFCONF will return the alias address, so
		 * there's no way to figure the NETMASK for these
		 * addresses. */
		if (ioctl(newsock, SIOCGIFADDR, &ir) < 0)
		    goto bail_errno;
		adr = ((struct sockaddr_in *)(&ir.ifr_addr))->sin_addr;
		if (adr.s_addr != sia.s_addr) {
		    char *buf;
		    buf = critstrdup(inet_ntoa(sia), "checkpeerlocal");
		    syslog(LOG_CRIT, "Problem: your kernel cannot deal with 4.4BSD-style IP aliases properly. "
			    "SIOCGIFADDR for interface %s address %s yields %s -> mismatch. "
			    "Configure Solaris-style aliases like %s:0 instead. Ignoring this interface, addresses in its range will be considered remote.",
			    ifr->ifr_name, buf, inet_ntoa(adr),
			    ifr->ifr_name);
		    D(printf("Kernel does not handle 4.4BSD-style IP alias for interface %s address %s, ignoring this interface.\n",
				ifr->ifr_name, buf));
		    free(buf);
		    continue;
		}
		memcpy(&ir, ifr, sizeof(struct ifreq));
		if (ioctl(newsock, SIOCGIFNETMASK, &ir) < 0)
		    goto bail_errno;
		msk = ((struct sockaddr_in *)(&ir.ifr_addr))->sin_addr;

		D(printf("address/netmask: %s/", inet_ntoa(adr)));
		D(printf("%s\n", inet_ntoa(msk)));

		if ((((struct sockaddr_in *)addr)->sin_addr.s_addr & msk.s_addr) 
			== (adr.s_addr & msk.s_addr)) {
		    D(printf("found\n"));
		    close(newsock);
		    mastr_delete(buf);
		    return 1;
		}
	    }
    }

    close(newsock);
    mastr_delete(buf);
    return 0;

bail_errno:
    close(newsock);
    mastr_delete(buf);
    return -1;
}

#ifdef TEST
#include <string.h>

int verbose = 0;
int debug = 0;

int main(void)
{
    int r;
    printf("checkpeerlocal returned: %d\n", (r = checkpeerlocal(0)));
    if (r == -1) printf("errno: %d (%s)\n", errno, strerror(errno));
    return 0;
}
#endif


syntax highlighted by Code2HTML, v. 0.9.1