/*****
*
* Copyright (C) 2003-2005 Nicolas Delon <nicolas@prelude-ids.org>
* All Rights Reserved
*
* This file is part of the Prelude program.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by 
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING.  If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
*
*****/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/poll.h>
#include <sys/param.h>
#include <sys/sysctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <netinet/ip_icmp.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <net/if.h>
#ifdef __DragonFly__
#	include <net/if_var.h>
#	include <net/pf/if_pflog.h>
#	include <net/pf/pfvar.h>
#else
#	include <net/if_pflog.h>
#	include <net/pfvar.h>
#endif
#include <netdb.h>
#include <pcap.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <net/bpf.h>
#include <errno.h>

#include <libprelude/prelude.h>
#include <libprelude/prelude-log.h>

#include "process_packet.h"
#include "pflogger.h"

struct packet_info {
	const char *ifname;
	unsigned int rnr;
	unsigned int reason;
	unsigned int action;
	unsigned int dir;

	char *source_address;
	char *target_address;
	uint8_t ttl;

	enum {
		ip_version_4 = AF_INET,
		ip_version_6 = AF_INET6
	}	ip_version;

	int	ip_proto_type;

	union {
		struct {
			uint16_t sport;
			uint16_t dport;
			uint8_t flags;
		}	tcp;
		struct {
			uint16_t sport;
			uint16_t dport;
		}	udp;
		struct {
			uint8_t type;
			uint8_t code;
		}	icmp;
	}	ip_proto;
};



static pcap_t *pcap;



static int in_addr_to_string(int af, prelude_string_t *string, const void *src, char *dst, size_t size)
{
	size_t len;

	if ( ! inet_ntop(af, src, dst, size) )
		return prelude_error_from_errno(errno);

	len = strlen(dst);

	prelude_string_set_ref_fast(string, dst, len);

	return 0;
}



static int process_hdr_icmp(idmef_service_t *source_service,
			    idmef_service_t *target_service,
			    const struct icmp *icmp_hdr,
			    struct packet_info *packet_info)
{
	packet_info->ip_proto.icmp.type = icmp_hdr->icmp_type;
	packet_info->ip_proto.icmp.code = icmp_hdr->icmp_code;

	return 0;
}



static int process_hdr_icmp6(idmef_service_t *source_service,
			     idmef_service_t *target_service,
			     const struct icmp6_hdr *icmp6_hdr,
			     struct packet_info *packet_info)
{
	packet_info->ip_proto.icmp.type = icmp6_hdr->icmp6_type;
	packet_info->ip_proto.icmp.code = icmp6_hdr->icmp6_code;

	return 0;
}



static int process_hdr_tcp(idmef_service_t *source_service,
			   idmef_service_t *target_service,
			   const struct tcphdr *tcp_hdr,
			   struct packet_info *packet_info)
{
	uint16_t sport, dport;

	sport = ntohs(tcp_hdr->th_sport);
	dport = ntohs(tcp_hdr->th_dport);

	idmef_service_set_port(source_service, sport);
	idmef_service_set_port(target_service, dport);

	packet_info->ip_proto.tcp.flags = tcp_hdr->th_flags;
	packet_info->ip_proto.tcp.sport = sport;
	packet_info->ip_proto.tcp.dport = dport;

	return 0;
}



static int process_hdr_udp(idmef_service_t *source_service,
			   idmef_service_t *target_service,
			   const struct udphdr *udp_hdr,
			   struct packet_info *packet_info)
{
	uint16_t sport, dport;

	sport = ntohs(udp_hdr->uh_sport);
	dport = ntohs(udp_hdr->uh_dport);

	idmef_service_set_port(source_service, sport);
	idmef_service_set_port(target_service, dport);

	packet_info->ip_proto.udp.sport = sport;
	packet_info->ip_proto.udp.dport = dport;

	return 0;
}


static int build_address(idmef_address_t *address, struct packet_info *packet_info,
			 const void *ip_addr, char *daddr, size_t daddr_len)
{
	prelude_string_t *string;
	int ret;

	idmef_address_set_category(address, ((packet_info->ip_version == ip_version_4) ?
					     IDMEF_ADDRESS_CATEGORY_IPV4_ADDR :
					     IDMEF_ADDRESS_CATEGORY_IPV6_ADDR));

        ret = idmef_address_new_address(address, &string);
        if ( ret < 0 )
                return ret;

	return in_addr_to_string(packet_info->ip_version, string, ip_addr, daddr, daddr_len);
}



static int build_source(idmef_alert_t *alert, const void *ip_addr, struct packet_info *packet_info)
{
	idmef_source_t *source;
	idmef_node_t *source_node;
	idmef_address_t *source_node_address;
	prelude_string_t *string;
	static char saddr[64];
	int ret;

	ret = idmef_alert_new_source(alert, &source, 0);
	if ( ret < 0 )
		return ret;

	ret = idmef_source_new_node(source, &source_node);
	if ( ret < 0 )
		return ret;

	ret = idmef_node_new_address(source_node, &source_node_address, 0);
	if ( ret < 0 )
		return ret;

	ret = build_address(source_node_address, packet_info, ip_addr, saddr, sizeof(saddr));
	if ( ret < 0 )
		return ret;

	if ( packet_info->dir == PF_IN ) {
		ret = idmef_source_new_interface(source, &string);
		if ( ret < 0 )
			return ret;
		prelude_string_set_ref(string, packet_info->ifname);
	}

	packet_info->source_address = saddr;

	return 0;
}



static int build_target(idmef_alert_t *alert, const void *ip_addr, struct packet_info *packet_info)
{
	idmef_target_t *target;
	idmef_node_t *target_node;
	idmef_address_t *target_node_address;
	prelude_string_t *string;
	static char daddr[64];
	int ret;

	ret = idmef_alert_new_target(alert, &target, 0);
	if ( ret < 0 )
		return ret;

	ret = idmef_target_new_node(target, &target_node);
	if ( ret < 0 )
		return ret;

	ret = idmef_node_new_address(target_node, &target_node_address, 0);
	if ( ret < 0 )
		return ret;

	ret = build_address(target_node_address, packet_info, ip_addr, daddr, sizeof(daddr));
	if ( ret < 0 )
		return ret;

	if ( packet_info->dir == PF_OUT ) {
		ret = idmef_target_new_interface(target, &string);
		if ( ret < 0 )
			return ret;
		prelude_string_set_ref(string, packet_info->ifname);
	}

	packet_info->target_address = daddr;

	return 0;
}



static int build_service_base(idmef_service_t *service, struct packet_info *packet_info)
{
	struct protoent *proto;
	prelude_string_t *string;
	int ret;

	if ( packet_info->ip_version == ip_version_4 )
		idmef_service_set_ip_version(service, 4);
	else
		idmef_service_set_ip_version(service, 6);

	idmef_service_set_iana_protocol_number(service, packet_info->ip_proto_type);

	proto = getprotobynumber(packet_info->ip_proto_type);
	if ( ! proto )
		return prelude_error_from_errno(errno);

	ret = prelude_string_new_dup(&string, proto->p_name);
	if ( ret < 0 )
		return ret;

	idmef_service_set_iana_protocol_name(service, string);

	return ret;
}



static int build_service(idmef_source_t *source, idmef_target_t *target,
			 const void *protocol_header,
			 struct packet_info *packet_info)
{
	idmef_service_t *source_service;
	idmef_service_t *target_service;
	int ret;

        ret = idmef_source_new_service(source, &source_service);
	if ( ret < 0 )
		return ret;

	ret = build_service_base(source_service, packet_info);
	if ( ret < 0 )
		return ret;

	ret = idmef_target_new_service(target, &target_service);
	if ( ret < 0 )
		return ret;

	ret = build_service_base(target_service, packet_info);
	if ( ret < 0 )
		return ret;

	switch ( packet_info->ip_proto_type ) {
	case IPPROTO_ICMP:
		return process_hdr_icmp(source_service, target_service,
					(const struct icmp *) protocol_header, packet_info);

	case IPPROTO_ICMPV6:
		return process_hdr_icmp6(source_service, target_service,
					 (const struct icmp6_hdr *) protocol_header, packet_info);

	case IPPROTO_TCP:
		return process_hdr_tcp(source_service, target_service,
				       (const struct tcphdr *) protocol_header, packet_info);

	case IPPROTO_UDP:
		return process_hdr_udp(source_service, target_service,
				       (const struct udphdr *) protocol_header, packet_info);

	default:
		/* nop */;
	}

	return 0;
}



static int process_hdr_ipv4(idmef_alert_t *alert, const struct ip *ip_hdr, 
			    struct packet_info *packet_info)
{
	int ret;

	packet_info->ip_version = ip_version_4;
	packet_info->ttl = ip_hdr->ip_ttl;
	packet_info->ip_proto_type = ip_hdr->ip_p;

	ret = build_source(alert, &ip_hdr->ip_src, packet_info);
	if ( ret < 0 )
		return ret;

	ret = build_target(alert, &ip_hdr->ip_dst, packet_info);
	if ( ret < 0 )
		return ret;

	return build_service(idmef_alert_get_next_source(alert, NULL),
			     idmef_alert_get_next_target(alert, NULL),
			     ((const void *) ip_hdr) + ip_hdr->ip_hl * 4,
			     packet_info);
}



static int process_hdr_ipv6(idmef_alert_t *alert, const struct ip6_hdr *ip6_hdr, 
			    struct packet_info *packet_info)
{
	int ret;

	packet_info->ip_version = ip_version_6;
	packet_info->ttl = ip6_hdr->ip6_hops;
	packet_info->ip_proto_type = ip6_hdr->ip6_nxt;

	ret = build_source(alert, &ip6_hdr->ip6_src, packet_info);
	if ( ret < 0 )
		return ret;

	ret = build_target(alert, &ip6_hdr->ip6_dst, packet_info);
	if ( ret < 0 )
		return ret;

	return build_service(idmef_alert_get_next_source(alert, NULL),
			     idmef_alert_get_next_target(alert, NULL),
			     ip6_hdr + 1, packet_info);
}



static int process_hdr_unknown_af(idmef_alert_t *alert, unsigned int af)
{
	idmef_classification_t *classification;
	prelude_string_t *text;
	int ret;

	ret = idmef_alert_new_classification(alert, &classification);
	if ( ret < 0 )
		return ret;

	ret = idmef_classification_new_text(classification, &text);
	if ( ret < 0 )
		return ret;

	return prelude_string_sprintf(text, "unknown protocol %u", af);	
}



static int build_impact_description_icmp(struct packet_info *packet_info,
					 char *action_str,
					 char *dir_str,
					 char *icmp,
					 char *buffer, size_t size)
{
	return snprintf(buffer, size,
			"OpenBSD PF %s an %s %s packet %s -> %s type:%hhu code:%hhu on interface %s (TTL:%hhu)",
			action_str, dir_str, icmp,
			packet_info->source_address, packet_info->target_address,
			packet_info->ip_proto.icmp.type, packet_info->ip_proto.icmp.code,
			packet_info->ifname, packet_info->ttl);
}



static int build_impact_description_tcp(struct packet_info *packet_info,
					char *action_str,
					char *dir_str,
					char *buffer, size_t size)
{
	static const char *tcp_flags_table[] = {
		"FIN", "SYN", "RST", "PUSH", "ACK", "URG", "ECE", "CWR"
	};
	char tcp_flags[64];
	int cnt;
	int len;
	int ret;

	len = 0;
	for ( cnt = 0; cnt < sizeof (tcp_flags_table) / sizeof (tcp_flags_table[0]); cnt++ ) {
		if ( packet_info->ip_proto.tcp.flags & (1 << cnt) ) {
			ret = snprintf(tcp_flags + len, sizeof(tcp_flags) - len,
				       "%s%s", len ? "|" : "", tcp_flags_table[cnt]);
			if ( ret < 0 )
				return ret;

			len += ret;
		}
	}

	return snprintf(buffer, size,
			"OpenBSD PF %s an %s TCP packet %s:%hu -> %s:%hu [%s] on interface %s (TTL:%hhu)",
			action_str, dir_str,
			packet_info->source_address, packet_info->ip_proto.tcp.sport,
			packet_info->target_address, packet_info->ip_proto.tcp.dport,
			tcp_flags, packet_info->ifname, packet_info->ttl);
}



static int build_impact_description_udp(struct packet_info *packet_info,
					char *action_str,
					char *dir_str,
					char *buffer, size_t size)
{
	return snprintf(buffer, size,
			"OpenBSD PF %s an %s UDP packet %s:%hu -> %s:%hu on interface %s (TTL:%hhu)",
			action_str, dir_str,
			packet_info->source_address, packet_info->ip_proto.udp.sport,
			packet_info->target_address, packet_info->ip_proto.udp.dport,
			packet_info->ifname, packet_info->ttl);
}



static int build_impact_description_default(struct packet_info *packet_info,
					    char *action_str,
					    char *dir_str,
					    char *buffer, size_t size)
{
	struct { char *name; int n; } ip_proto_table[] = CTL_IPPROTO_NAMES;
	char *proto_name = "unknown";

	if ( packet_info->ip_proto_type < sizeof (ip_proto_table) / sizeof (ip_proto_table[0]) &&
	     ip_proto_table[packet_info->ip_proto_type].name )
		proto_name = ip_proto_table[packet_info->ip_proto_type].name;

	return snprintf(buffer, size,
			"OpenBSD PF %s an %s %s packet %s -> %s on interface %s (TTL:%hhu)",
			action_str, dir_str,
			proto_name,
			packet_info->source_address,
			packet_info->target_address,
			packet_info->ifname,
			packet_info->ttl);
}



static int build_impact_description(idmef_impact_t *impact,
				    struct packet_info *packet_info,
				    char *action_str)
				    
{
	static char impact_description[256];
	char *dir_str;
	prelude_string_t *string;
	int ret;

	switch ( packet_info->dir ) {
	case PF_IN:
		dir_str = "incoming";
		break;

	case PF_OUT:
		dir_str = "outgoing";
		break;

	default:
		prelude_log(PRELUDE_LOG_ERR, "unknown direction %u\n", packet_info->dir);
		return -1;
	}

	switch ( packet_info->ip_proto_type ) {
	case IPPROTO_ICMP:
		ret = build_impact_description_icmp(packet_info, action_str, dir_str, "ICMP",
						    impact_description, sizeof (impact_description));
		break;

	case IPPROTO_ICMPV6:
		ret = build_impact_description_icmp(packet_info, action_str, dir_str, "ICMP6",
						    impact_description, sizeof (impact_description));
		break;

	case IPPROTO_TCP:
		ret = build_impact_description_tcp(packet_info, action_str, dir_str,
						   impact_description, sizeof (impact_description));
		break;

	case IPPROTO_UDP:
		ret = build_impact_description_udp(packet_info, action_str, dir_str,
						   impact_description, sizeof (impact_description));
		break;

	default:
		ret = build_impact_description_default(packet_info, action_str, dir_str,
						       impact_description, sizeof (impact_description));
	}

	if ( ret < 0 || ret >= sizeof (impact_description) )
		return prelude_error(PRELUDE_ERROR_GENERIC);

	ret = idmef_impact_new_description(impact, &string);
	if ( ret < 0 )
		return ret;

	prelude_string_set_ref(string, impact_description);

	return 0;
}



static int build_impact(idmef_alert_t *alert,
			struct packet_info *packet_info,
			char *action_str)
{
	idmef_assessment_t *assessment;
	idmef_impact_t *impact;
	int ret;

	ret = idmef_alert_new_assessment(alert, &assessment);
	if ( ret < 0 )
		return ret;

	ret = idmef_assessment_new_impact(assessment, &impact);
	if ( ret < 0 )
		return ret;

	idmef_impact_set_severity(impact, IDMEF_IMPACT_SEVERITY_LOW);

	if ( packet_info->action == PF_DROP ) {
		idmef_impact_set_completion(impact, IDMEF_IMPACT_COMPLETION_FAILED);
		idmef_impact_set_type(impact, IDMEF_IMPACT_TYPE_RECON);

	} else {
		idmef_impact_set_completion(impact, IDMEF_IMPACT_COMPLETION_SUCCEEDED);
	}

	return build_impact_description(impact, packet_info, action_str);
}



static int build_classification(idmef_alert_t *alert, char *action_str)
{
	static char classification_text[256];
	idmef_classification_t *classification;
	prelude_string_t *string;
	int ret;

	ret = idmef_alert_new_classification(alert, &classification);
	if ( ret < 0 )
		return ret;

	ret = snprintf(classification_text, sizeof(classification_text), "Packet %s by PF firewall", action_str);
	if ( ret < 0 || ret >= sizeof(classification_text) )
		return prelude_error(PRELUDE_ERROR_GENERIC);

	ret = idmef_classification_new_text(classification, &string);
	if ( ret < 0 )
		return ret;

	prelude_string_set_ref(string, classification_text);

	return 0;
}



static int build_additional_data(idmef_alert_t *alert, struct packet_info *packet_info)
{
	idmef_additional_data_t *adata;
	prelude_string_t *string;
	int ret;

	ret = idmef_alert_new_additional_data(alert, &adata, 0);
	if ( ret < 0 )
		return ret;

	ret = idmef_additional_data_new_meaning(adata, &string);
	if ( ret < 0 )
		return ret;

	prelude_string_set_constant(string, "match rule number");
	
	idmef_additional_data_set_integer(adata, packet_info->rnr);

	return 0;
}



static int build_others(idmef_alert_t *alert, struct packet_info *packet_info)
{
	char *action_str;
	int ret;

	switch ( packet_info->action ) {
	case PF_PASS:
		action_str = "accepted";
		break;

	case PF_DROP:
		action_str = "blocked";
		break;

	case PF_SCRUB:
		action_str = "defragmented";
		break;

	default:
		action_str = "processed";
	}

	ret = build_classification(alert, action_str);
	if ( ret < 0 )
		return ret;

	ret = build_impact(alert, packet_info, action_str);
	if ( ret < 0 )
		return ret;

	ret = build_additional_data(alert, packet_info);
	if ( ret < 0 )
		return ret;

	return 0;
}



static int process_hdr_pflog(idmef_alert_t *alert, const struct pfloghdr *pflog_hdr)
{
	struct packet_info packet_info;
	unsigned int af;
	int ret;

	memset(&packet_info, 0, sizeof (packet_info));

	packet_info.ifname = pflog_hdr->ifname;

#ifdef DIOCOSFPGET /* PF OS fingerprinting is available, it meens that we run OpenBSD >= 3.4 */
	af = pflog_hdr->af;
	packet_info.rnr = ntohl(pflog_hdr->rulenr);
        packet_info.reason = pflog_hdr->reason;
        packet_info.action = pflog_hdr->action;
        packet_info.dir = pflog_hdr->dir;
#else /* OpenBSD < 3.4 */
	af = ntohl(pflog_hdr->af);
	packet_info.rnr = ntohs(pflog_hdr->rnr);
	packet_info.reason = ntohs(pflog_hdr->reason);
	packet_info.action = ntohs(pflog_hdr->action);
	packet_info.dir = ntohs(pflog_hdr->dir);
#endif /* DIOCOSFPGET */

	switch ( af ) {
	case AF_INET:
		ret = process_hdr_ipv4(alert, (const struct ip *) (pflog_hdr + 1), &packet_info);
		break;

	case AF_INET6:
		ret = process_hdr_ipv6(alert, (const struct ip6_hdr *) (pflog_hdr + 1), &packet_info);
		break;

	default:
		return process_hdr_unknown_af(alert, af);
	}

	if ( ret < 0 )
		return ret;

	ret = build_others(alert, &packet_info);
	if ( ret < 0 )
		return ret;

	return 0;
}



static int process_hdr_pcap(idmef_alert_t *alert, const struct pcap_pkthdr *pcap_hdr)
{
	idmef_time_t *detect_time;
	int ret;

	ret = idmef_alert_new_detect_time(alert, &detect_time);

       	idmef_time_set_from_time(detect_time, &pcap_hdr->ts.tv_sec);
	idmef_time_set_usec(detect_time, pcap_hdr->ts.tv_usec);

	return 0;
}



static void process_packet(u_char *ptr, const struct pcap_pkthdr *pcap_hdr,
			   const u_char *data)
{
	const struct pfloghdr *pflog_hdr;
	idmef_message_t *message;
	idmef_alert_t *alert;
	int ret;

	ret = idmef_message_new(&message);
	if ( ret < 0 )
		goto error;

	ret = idmef_message_new_alert(message, &alert);
	if ( ret < 0 )
		goto error;

	pflog_hdr = (const struct pfloghdr *) data;

	if ( pcap_hdr ) {
		ret = process_hdr_pcap(alert, pcap_hdr);
		if ( ret < 0 )
			goto error;
	}

	ret = process_hdr_pflog(alert, pflog_hdr);
	if ( ret < 0 )
		goto error;

	ret = pflogger_send_alert(message);
	idmef_message_destroy(message);
	if ( ret < 0 )
		goto error;

	return;

 error:
	prelude_perror(ret, "error while processing packet");
}



int process_packet_init(const char *interface, int snaplen)
{
	char buffer[PCAP_ERRBUF_SIZE];

	pcap = pcap_open_live((char *) interface, snaplen, 0, 500, buffer);
	if ( ! pcap ) {
		prelude_log(PRELUDE_LOG_ERR, "Could not start Prelude PFlogger: %s\n", buffer);
		exit(1);
	}

	return 0;
}



void process_packet_stop(void)
{
	struct pcap_stat ps;
	double ratio;

	if ( pcap_stats(pcap, &ps) < 0 ) {
		prelude_log(PRELUDE_LOG_ERR, "pcap_stats: %s\n", pcap_geterr(pcap));
		return;
	}

	ratio = ps.ps_recv ? (((double) ps.ps_drop / (double) ps.ps_recv) * 100) : 0;

	prelude_log(PRELUDE_LOG_INFO, "%u packets received\n", ps.ps_recv);
	prelude_log(PRELUDE_LOG_INFO, "%u (%.2lf%%) packets dropped\n", ps.ps_drop, ratio);

	pcap_close(pcap);
}



/*
 * We use poll + pcap_dispatch instead of a simple pcap_loop
 * because the read syscall on bpf device won't give back to 
 * the scheduler, due to the buggy implementation of thread
 * on OpenBSD
 */

int process_packet_mainloop(void)
{
	int fd;
	struct pollfd pfd;
	struct pcap_stat ps_prev, ps_new;
	int packets_to_read;
	u_int immediate_on = 1;

	fd = pcap_fileno(pcap);

	pfd.fd = fd;
	pfd.events = POLLIN;

	memset(&ps_new, 0, sizeof (ps_new));

	/*
	 * Get packets as soon as they are available in kernel side
	 */

	if ( ioctl(fd, BIOCIMMEDIATE, &immediate_on) < 0 )
		return prelude_error_from_errno(errno);

	while ( 1 ) {
		if ( poll(&pfd, 1, -1) < 0 )
			return prelude_error_from_errno(errno);

		packets_to_read = 0;

		/*
		 * Process the newly arrived packet and the other packets eventually
		 * buffered by pcap
		 */

		do {
			ps_prev = ps_new;

			if ( pcap_dispatch(pcap, 1, process_packet, NULL) < 0 ) {
				prelude_log(PRELUDE_LOG_ERR, "pcap_dispatch: %s\n", pcap_geterr(pcap));
				return -1;
			}

			if ( pcap_stats(pcap, &ps_new) < 0 ) {
				prelude_log(PRELUDE_LOG_ERR, "pcap_stats: %s\n", pcap_geterr(pcap));
				return -1;
			}

			packets_to_read += ps_new.ps_recv - ps_prev.ps_recv - 1;

		} while ( packets_to_read > 0 );
	}

	return 0;
}
