/*
 * All functions for BSD's pf are here.
 *
 * This file is based largely on OpenBSD 4.1's spamlogd.c.
 *
 * Copyright (c) 2008-2009 Michael Stapelberg <michael+mxallowd@stapelberg.de>
 * Copyright (c) 2006 Henning Brauer <henning@openbsd.org>
 * Copyright (c) 2006 Berk D. Demir.
 * Copyright (c) 2004-2007 Bob Beck.
 * Copyright (c) 2001 Theo de Raadt.
 * Copyright (c) 2001 Can Erkin Acar.
 * All rights reserved
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/if_pflog.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <arpa/inet.h>
#include <net/pfvar.h>
#include <pcap.h>
#include <string.h>
#include <errno.h>

#include "mxallowd.h"
#include "log.h"
#include "whitelist.h"

pcap_t *hpcap = NULL;
char *pflogif = "pflog0";
char *pcap_filter = "port 25";
char errbuf[PCAP_ERRBUF_SIZE];
#define PCAPSNAP		512
#define PCAPTIMO		500	/* ms */
#define PCAPOPTZ		1	/* optimize filter */
#define MIN_PFLOG_HDRLEN	45

static void handlePacket(u_char *user, const struct pcap_pkthdr *h, const u_char *sp) {
	sa_family_t		 af;
	u_int8_t		 hdrlen;
	u_int32_t		 caplen = h->caplen;
	const struct ip		*ip = NULL;
	const struct ip6_hdr	*ip6 = NULL;
	const struct pfloghdr	*hdr;

	cleanup_whitelist();

	char dest_address[INET6_ADDRSTRLEN+1],
	     source_address[INET6_ADDRSTRLEN+1];

	memset(dest_address, '\0', INET6_ADDRSTRLEN+1);
	memset(source_address, '\0', INET6_ADDRSTRLEN+1);

	/* Sanity checks for the packet */
	hdr = (const struct pfloghdr *)sp;
	if (hdr->length < MIN_PFLOG_HDRLEN) {
		slog("invalid pflog header length (%u/%u). "
		     "packet dropped.\n", hdr->length, MIN_PFLOG_HDRLEN);
		return;
	}
	hdrlen = BPF_WORDALIGN(hdr->length);

	if (caplen < hdrlen) {
		slog("pflog header larger than caplen (%u/%u). "
		     "packet dropped.\n", hdrlen, caplen);
		return;
	}

	af = hdr->af;
	const void *srcptr, *dstptr;
	if (af == AF_INET) {
		ip = (const struct ip *)(sp + hdrlen);
		srcptr = &(ip->ip_src);
		dstptr = &(ip->ip_dst);
	} else if (af == AF_INET6) {
		ip6 = (const struct ip6_hdr *)(sp + hdrlen);
		srcptr = &(ip6->ip6_src);
		dstptr = &(ip6->ip6_dst);
	} else return;

	inet_ntop(af, srcptr, source_address, sizeof(source_address));
	inet_ntop(af, dstptr, dest_address, sizeof(dest_address));

	/* Let's see if the packet was sent to MX1 */
	if (is_included(fake_mailservers, dest_address)) {
		/* This packet was sent to MX1, whitelist the sender for MX2 */
		slog("Successful connection from %s to a fake mailserver, adding to whitelist\n", source_address);
		add_to_whitelist(source_address, NULL, false, af, srcptr);
	} else if (is_included(real_mailservers, dest_address)) {
		/* This packet was sent to MX2, let's see what to do */
		if (hdr->action == PF_PASS)
			slog("Successful connection from %s to a real mailserver\n", source_address);
		else {
			/* The sender is not whitelisted, so the packet was dropped */
			slog("Dropping connection attempt from %s to a real mailserver\n", source_address);
			blocked_attempts++;
		}
	}
}

void pcap_init() {
	pcap_handler ph = handlePacket;
	struct bpf_program bpfp;

	if ((hpcap = pcap_open_live(pflogif, PCAPSNAP, 1, PCAPTIMO, errbuf)) == NULL)
		dief("Failed to initialize: %s\n", errbuf);

	if (pcap_datalink(hpcap) != DLT_PFLOG) {
		pcap_close(hpcap);
		dief("Invalid datalink type\n");
	}

	if (pcap_compile(hpcap, &bpfp, pcap_filter, PCAPOPTZ, 0) == -1 ||
	    pcap_setfilter(hpcap, &bpfp) == -1)
		dief("%s\n", pcap_geterr(hpcap));

	pcap_freecode(&bpfp);

#ifdef BIOCLOCK
	if (ioctl(pcap_fileno(hpcap), BIOCLOCK) < 0)
		dief("BIOCLOCK: %s\n", strerror(errno));
#endif

	pcap_loop(hpcap, -1, ph, NULL);
}
