// Copyright (C) FIT VUT
// Petr Lampa <lampa@fit.vutbr.cz>
// $Id$
// vi:set ts=8 sts=8 sw=8:
//
//#define SQL
#include "common.h"
#include "ethernet.h"
#include "ipv6.h"
#include "ipv4.h"
#include "icmpv6.h"
#include "arp.h"
#include "cfgparser.h"
#include "pcap.h"
#include "util.h"
#ifdef SQL
#include "mysql.h"
#else
#include "mydbm.h"
#endif
extern "C" {
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
}
#include <ctime>
#include <vector>
#include <map>
#include <iostream>
#include <fstream>
#include <cctype>

// remove bogus router delay
#define MIN_DELAY 500
#define MAX_DELAY 1500
#define MLD_QUERY_INTERVAL 125

// max addresses per MAC
#define MAX_ADDRS 20
#define ARP_STAT 255

class Stats {
private:
	time_t last[256];
	time_t last1m[256];
	long long _count[256];	// total count of RA
	long count1s[256];
	long max1s[256];
	long count1m[256];
	long max1m[256];
	string _name[256];
public:
	Stats() { }
	long update(const string &name, unsigned type, time_t cur_time) 
	{
		if (type > 255) return 0;
		if (_count[type] == 0) _name[type] = name;
		_count[type]++;
		if (cur_time - last[type] <= 1) {	
			count1s[type]++;
		} else {
			count1s[type] = 1;
		}
		last[type] = cur_time;
		if (count1s[type] > max1s[type]) max1s[type] = count1s[type];
		time_t cur1m = cur_time - cur_time%60;
		if (last1m[type] == cur1m) {	
			count1m[type]++;
		} else {
			count1m[type] = 1;
		}
		last1m[type] = cur1m;
		if (count1m[type] > max1m[type]) max1m[type] = count1m[type];
		return count1s[type];
	}
	long count(unsigned type) const { 
		if (type > 255) return 0;
		return _count[type]; 
	}
	string to_string(unsigned type)
	{
		if (type > 255) return 0;
		string str = _name[type];
		str += ": total ";
		str += cvt_int(_count[type]);
		str += " ";
		str += cvt_int(max1s[type]);
		str += "/s ";
		str += cvt_int(max1m[type]);
		str += "/m";
		return str;
	}
};

class Subnet {
public:
	IPv6Address prefix;
	int plen;
	IPv6Address addrs[MAX_ADDRS];
	int naddrs;
	MacAddress mac;
	string dev;
	time_t last_querier;
	Subnet(): prefix(), plen(0), naddrs(0), mac(),dev(),last_querier(0) { }

	bool add_addr(IPv6Address &adr) { 
		if (naddrs >= MAX_ADDRS) return false;
		addrs[naddrs++] = adr;
		return true;
	}
};

// email events
enum { NEW_NODE=1, UPD_NODE=2, INV_RA=4, INV_NA=8, DAD_DOS=16, RA_RATE=32 };
class EmailEv {
public:
	unsigned event;
	string email;
	EmailEv(): event(0), email() { }
};
typedef vector<EmailEv> EmailEvents;
EmailEvents email_events;

enum { INIT_NSL=1, INIT_RS=2, INIT_NSG=4, MLD_BAD=0x1000, DAD_RS_BAD=0x2000 };

// node will persist for program lifetime, no destruction 
struct Node {
	MacAddress mac;
	time_t mac_ts;
	IPv6Address lla;
	time_t lla_ts;
	time_t lla_mld_ts;
	int naddrs;
	int nipv4;
	int status;
	unsigned email;
	IPv4Address ipv4[MAX_ADDRS];
	time_t ipv4_ts[MAX_ADDRS];
	IPv6Address addrs[MAX_ADDRS];	// arbitrary low to fit in DBM data
	time_t addrs_ts[MAX_ADDRS];
	time_t mld_ts[MAX_ADDRS];
};
typedef map<const IPv6Address,Node *> NC6;
typedef map<const MacAddress,Node *> NCM;
typedef map<const string,Stats *> TStats;

NC6 nc6;
NCM ncm;
#ifdef SQL
Mysql_DB *db;
string db_host,db_db,db_user,db_pass;
#else 
MyDBM *mydb;
string db_name;
#endif
PCAP pcap;
bool finish;
bool dump_stat;
bool dflag;
bool uflag;
time_t start_time;
int ra_limit;		// RA pers sec limit
int ra_email;		// RA rate check email interval
time_t ra_last_email;
time_t ra_rate_time;
string ra_rate_msg;
time_t cur_time;

typedef vector<Subnet> Subnets;
Subnets subnets;
TStats stats;
typedef map<const string, MacAddress> Macs;
Macs macs;
struct PQE {
	Packet *p;
	const string *dev;
};
typedef map<unsigned long long, struct PQE> PurgeQueue;
PurgeQueue purge_queue;

// 64bit unix time in ms
unsigned long long get_time()
{
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec*1000+tv.tv_usec/1000;
}

#ifdef SQL
void read_db()
{
	Node *n = NULL;
	string mac;
	int i = 0;

	if (debug) cerr << "reading db" << endl;
	db = new Mysql_DB(db_host, db_db, db_user, db_pass);
	if (!db->connect()) {
		cerr << "cannot open database " << db_host << endl;
		return;
	}
	Query stmt(*db);
	if (!stmt.execute("CREATE TABLE IF NOT EXISTS `node` (	\
		`mac` varchar(20) NOT NULL default '',	\
		`mac_first` int(11) NOT NULL default '0',	\
		`mac_ts` int(11) NOT NULL default '0',	\
		`mac_refs` bigint(20) NOT NULL default '0',	\
		`status` int(11) NOT NULL default '0',	\
		`email` int(11) NOT NULL default '0',	\
		PRIMARY KEY  (`mac`)	\
		) TYPE=MyISAM;")) {
		cerr << "create table sql error " << db->error() << endl;
		return;
	}
	if (!stmt.execute("CREATE TABLE IF NOT EXISTS `node_addr` (	\
		`addr_mac` varchar(20) NOT NULL default '',	\
		`address` varchar(40) NOT NULL default '',	\
		`addr_type` enum('ipv4','lla','gua') NOT NULL default 'ipv4',	\
		`addr_first` int(11) NOT NULL default '0',	\
		`addr_ts` int(11) NOT NULL default '0',	\
		`addr_refs` bigint(20) NOT NULL default '0',	\
		KEY `addr_mac` (`addr_mac`)	\
		) TYPE=MyISAM;")) {
		cerr << "create table sql error " << db->error() << endl;
		return;
	}
	if (!stmt.execute("select * from node left join node_addr on mac=addr_mac order by node.mac,addr_ts desc")) {
		cerr << "select sql error " << db->error() << endl;
		return;
	}
	while (stmt.fetch_row()) {
		if (stmt["mac"] != mac) {
			if (mac.length() && debug) {
				cerr << "MAC " << n->mac << " " << ctime(&n->mac_ts);
				for (int j = 0; j < n->nipv4; j++) {
					cerr << "    IPv4 " << n->ipv4[j] << " " << ctime(&n->ipv4_ts[j]);
				}
				if (!n->lla.is_unspecified()) {
					cerr << "    LLA " << n->lla << " ";
					if (n->status & MLD_BAD) cerr << "(LLA without MLD SN report) ";
					cerr << ctime(&n->lla_ts);
				}
				for (int j = 0; j < n->naddrs; j++) {
					cerr << "    IPv6 " << n->addrs[j] << " " << ctime(&n->addrs_ts[j]);
				}
			}
			n = static_cast<Node *>(operator new(sizeof(Node)));
			memset(n, 0, sizeof(Node));
			mac = stmt["mac"];
			n->mac.addr(mac);
			n->mac_ts = stmt.get_int("mac_ts");
			ncm[n->mac] = n;
		}
		if (stmt["addr_type"] == NULL) {
			// no address
		} else
		if (strcmp(stmt["addr_type"], "ipv4") == 0) {
			if (n->nipv4 < MAX_ADDRS) {
				n->ipv4[n->nipv4].addr(stmt["address"]);
				n->ipv4_ts[n->nipv4] = stmt.get_int("addr_ts");
				n->nipv4++;
			}
		} else
		if (strcmp(stmt["addr_type"], "lla") == 0) {	// TODO - multiple LLA
			n->lla.addr(stmt["address"]);
			n->lla_ts = stmt.get_int("addr_ts");
			if (!n->lla.is_unspecified()) nc6[n->lla] = n;
		} else
		if (strcmp(stmt["addr_type"], "gua") == 0) {
			if (n->naddrs < MAX_ADDRS) {
				n->addrs[n->naddrs].addr(stmt["address"]);
				n->addrs_ts[n->naddrs] = stmt.get_int("addr_ts");
				nc6[n->addrs[n->naddrs]] = n;
				n->naddrs++;
			}
		}
		i++;
	}
}

void write_db(Node *n)
{
	if (!db) return;
	Query stmt(*db);
	string q = "update node set mac_ts=";
	q += cvt_int(n->mac_ts);
	q += ",status=";
	q += cvt_int(n->status);
	q += ",email=";
	q += cvt_int(n->email);
	q += ",mac_refs=mac_refs+1";
	q += " where mac='"+n->mac.to_string()+"'";
	if (!stmt.execute(q)) {
		cerr << "update sql error " << db->error() << endl;
		return;
	}
	if (stmt.affected_rows() == 0) {	// no update?
		q = "insert into node set mac='"+n->mac.to_string();
		q += "',mac_ts=";
		q += cvt_int(n->mac_ts);
		q += ",mac_first=";
		q += cvt_int(n->mac_ts);
		q += ",status=";
		q += cvt_int(n->status);
		q += ",email=";
		q += cvt_int(n->email);
		if (!stmt.execute(q)) {
			cerr << "insert sql error " << db->error() << endl;
			return;
		}
		// ok?
		if (stmt.affected_rows() == 0) return;
	}
}

void update_address(Node *n, const string &type, const string &addr, time_t ts)
{
	if (!db) return;
	Query stmt(*db);
	string q = "update node_addr set addr_ts=";
	q += cvt_int(ts);
	q += ",addr_refs=addr_refs+1 where addr_mac='"+n->mac.to_string()+"' and addr_type='"+type+"' and address='"+addr+"'";
	if (!stmt.execute(q)) {
		cerr << "update sql error " << db->error() << endl;
		return;
	}
	if (stmt.affected_rows() == 0) {	// no update?
		q = "insert into node_addr set addr_mac='"+n->mac.to_string();
		q += "',addr_type='"+type+"',address='"+addr+"',addr_ts=";
		q += cvt_int(ts);
		q += ",addr_first=";
		q += cvt_int(ts);
		if (!stmt.execute(q)) {
			cerr << "insert sql error " << db->error() << endl;
			return;
		}
		// ok?
		if (stmt.affected_rows() == 0) return;
	}
}

void close_db()
{
	delete db;
}
#else
void read_db()
{
	Node *n;
	void *m;
	string mac;
	size_t sz;
	int i = 0;

	if (debug) cerr << "reading db" << endl;
	mydb = new MyDBM(db_name, true);
	while ((i==0?mydb->fetch_first(mac, m, sz):mydb->fetch_next(mac, m, sz))) {
		if (sz != sizeof(Node)) {
			if (i == 0) cerr << "Size of Node changed, deleting old db" << endl;
			mydb->remove(mac);
			i++;
			continue;
		}
		n = static_cast<Node *>(operator new(sizeof(Node)));
		memcpy(n, m, sizeof(Node));
		ncm[n->mac] = n;
		if (!n->lla.is_unspecified()) nc6[n->lla] = n;
		for (int j = 0; j < n->naddrs; j++) {
			nc6[n->addrs[j]] = n;
		}
		if (debug) {
			cerr << "MAC " << n->mac << " " << ctime(&n->mac_ts);
			for (int j = 0; j < n->nipv4; j++) {
				cerr << "    IPv4 " << n->ipv4[j] << " " << ctime(&n->ipv4_ts[j]);
			}
			if (!n->lla.is_unspecified()) {
				cerr << "    LLA " << n->lla << " ";
				if (n->status & MLD_BAD) cerr << "(LLA without MLD SN report) ";
 				cerr << ctime(&n->lla_ts);
			}
			for (int j = 0; j < n->naddrs; j++) {
				cerr << "    IPv6 " << n->addrs[j] << " " << ctime(&n->addrs_ts[j]);
			}
		}
		i++;
	}
}

void write_db(Node *n)
{
	if (mydb) mydb->insert_key(n->mac.to_string(), n, sizeof(Node));
}

void update_address(Node *n, const string &type, const string &addr, time_t ts)
{
	if (type == "ipv4") write_db(n);
}

void close_db()
{
	if (mydb) delete mydb;
}
#endif

void read_config()
{
	cfgparser cfg;
	ifstream f("ndwatch.cfg");
	if (!f.is_open()) {
		f.open("/etc/ndwatch.cfg");
		if (!f.is_open()) {
			f.open("/usr/local/etc/ndwatch.cfg");
			if (!f.is_open()) {
				cerr << "ndwatch.cfg not found" << endl;
				return;
			}
		}
	}
	int ln = 0;
	int nmac = 0;
	while (!f.eof()) {
		char line[1024];
		f.getline(line, sizeof(line));
		ln++;
		cfg.set_line(line);
		if (!cfg.skip_ws()) continue;
		if (cfg.curchar() == '#') continue;
		string keyw = cfg.get_word();
		if (keyw == "") continue;
		if (keyw == "nodes") {
#ifdef SQL
			// nodes host database user password
			if (!cfg.skip_ws() ||
			    cfg.curchar() == '#') {
				cerr << "expecting database server hostname at line " << ln << endl;
				continue;
			}
			db_host = cfg.get_word();
			if (!cfg.skip_ws() ||
			    cfg.curchar() == '#') {
				cerr << "expecting database name at line " << ln << endl;
				continue;
			}
			db_db = cfg.get_word();
			if (!cfg.skip_ws() ||
			    cfg.curchar() == '#') {
				cerr << "expecting database user name at line " << ln << endl;
				continue;
			}
			db_user = cfg.get_word();
			if (!cfg.skip_ws() ||
			    cfg.curchar() == '#') {
				cerr << "expecting database user password at line " << ln << endl;
				continue;
			}
			db_pass = cfg.get_word();
			read_db();
#else
			// nodes filename
			if (!cfg.skip_ws()) {
				cerr << "expecting nodes filename at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			db_name = cfg.get_word();
			if (db_name == "") {
				cerr << "invalid nodes filename at line " << ln << endl;
				continue;
			}
			read_db();
#endif
		} else
		if (keyw == "dev") {
			// interface name
			if (!cfg.skip_ws()) {
				cerr << "expecting interface name at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			string name = cfg.get_word();
			if (name == "") {
				cerr << "invalid interface name at line " << ln << endl;
				continue;
			}
			subnets.back().dev = name;
			stats[name] = new Stats;
			if (debug) cerr << "dev " << name << endl;
		} else
		if (keyw == "ra_limit") {
			// ra_limit ra/s email_interval
			if (!cfg.skip_ws()) {
				cerr << "expecting ra limit at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			if (!cfg.get_number(ra_limit, 1, 1000)) {
				cerr << "invalid ra limit at line " << ln << endl;
				continue;
			}
			if (!cfg.skip_ws()) {
				cerr << "expecting ra limit at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			if (!cfg.get_number(ra_email, 1, 100000)) {
				cerr << "invalid email interval at line " << ln << endl;
				continue;
			}
			if (debug) cerr << "ra_limit " << ra_limit << " " << ra_email << endl;
		} else
		if (keyw == "email") {
			if (!cfg.skip_ws()) {
				cerr << "expecting email at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			EmailEv mailev;
			mailev.email = cfg.get_word();
			if (mailev.email == "" || mailev.email.find('@') == string::npos) {
				cerr << "invalid email at line " << ln << endl;
				continue;
			}
			if (!cfg.skip_ws()) {
				cerr << "expecting event specification at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			string ev;
			while ((ev = cfg.get_word()) != "") {
				if (ev == "new_node") {
					mailev.event |= NEW_NODE;
				} else
				if (ev == "upd_node") {
					mailev.event |= UPD_NODE;
				} else
				if (ev == "invalid_ra") {
					mailev.event |= INV_RA;
				} else
				if (ev == "invalid_na") {
					mailev.event |= INV_NA;
				} else
				if (ev == "dad_dos") {
					mailev.event |= DAD_DOS;
				} else
				if (ev == "ra_rate") {
					mailev.event |= RA_RATE;
				} else {
					cerr << "invalid event name at line " << ln << endl;
				}
				if (!cfg.skip_ws()) break;
				if (cfg.curchar() == '#') break;
			}
			email_events.push_back(mailev);
			if (debug) cerr << "mail " << mailev.email << " events " << cvt_hex(mailev.event) << endl;
		} else
		if (keyw == "vlan") {
			// vlan vid
			if (!cfg.skip_ws()) {
				cerr << "expecting vlan id at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			int vid;
			if (!cfg.get_number(vid, 0, 4095)) {
				cerr << "invalid vlan id at line " << ln << endl;
				continue;
			}
			if (debug) cerr << "vlan " << vid << endl;
		} else
		if (keyw == "subnet") {
			// subnet prefix/plen
			if (!cfg.skip_ws()) {
				cerr << "expecting subnet ipv6 prefix/x at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() == '#') continue;
			Subnet s;
			if (!cfg.get_ipv6addr("/#", s.prefix)) {
				cerr << "expecting subnet ipv6 prefix at line " << ln << endl;
				continue;
			}
			if (cfg.curchar() != '/') {
				cerr << "expecting subnet ipv6 prefix at line " << ln << endl;
				continue;
			}
			cfg.skip_char();
			if (!cfg.get_number(s.plen, 0, 128)) {
				cerr << "subnet ipv6 prefix length is not a number 0..128 at line " << ln << endl;
				continue;
			}
			if (debug) cerr << "subnet " << s.prefix << "/" << s.plen << endl;
			subnets.push_back(s);
			nmac = 0;
		} else
		if (keyw == "mac") {
			if (nmac > 0) {	// copy current subnet
				Subnet s;
				s.prefix = subnets.back().prefix;
				s.plen = subnets.back().plen;
				subnets.push_back(s);
			}
			// mac MAC LLA GUA ...
			MacAddress mac;
			IPv6Address lla,gua;
			if (!cfg.skip_ws() || cfg.curchar() == '#') {
				cerr << "expecting router mac address at line " << ln << endl;
				continue;
			}
			if (!cfg.get_macaddr("#", mac)) {
				cerr << "router mac address is not valid ethernet address at line " << ln << endl;
				continue;
			}

			if (!cfg.skip_ws() || cfg.curchar() == '#') {
				cerr << "expecting router ipv6 link local address at line " << ln << endl;
				continue;
			}
			if (!cfg.get_ipv6addr("#", lla)) {
				cerr << "expecting router ipv6 link local address at line " << ln << endl;
				continue;
			}
			if (!lla.is_linklocal()) {
				cerr << "router address is not valid link local ipv6 address at line " << ln << endl;
				continue;
			}

			if (!cfg.skip_ws() || cfg.curchar() == '#') {
				cerr << "expecting router ipv6 global unicast address at line " << ln << endl;
				continue;
			}
			if (!cfg.get_ipv6addr("#", gua)) {
				cerr << "expecting router ipv6 global unicast address at line " << ln << endl;
				continue;
			}
			if (!gua.is_globalunicast()) {
				cerr << "router address is not valid global unicast ipv6 address at line " << ln << endl;
				continue;
			}
			nmac++;
			subnets.back().mac = mac;
			subnets.back().add_addr(lla);
			subnets.back().add_addr(gua);

			while (cfg.skip_ws() && cfg.curchar() != '#') {
				if (!cfg.get_ipv6addr("#", gua)) {
					cerr << "expecting router ipv6 global unicast address at line " << ln << endl;
					continue;
				}
				if (!gua.is_globalunicast()) {
					cerr << "router address is not valid global unicast ipv6 address at line " << ln << endl;
					continue;
				}
				subnets.back().add_addr(gua);
			}
			if (debug) cerr << "  mac " << subnets.back().mac << " lla " << subnets.back().addrs[0] << " gua " << subnets.back().addrs[1] << endl;
		} else {
			cerr << "unknown keyword " << keyw << " at line " << ln << endl;
		}
	}
	f.close();
}

void send_mail(unsigned event, const MacAddress &mac, const string &msg);

void send_upd_mail(unsigned event, Node *n) 
{
	string msg;
	if ((n->email & NEW_NODE) == 0) {
		msg = "New node MAC " + n->mac.to_string() + "\n";
		n->email |= UPD_NODE;
	} else msg = "Update node MAC " + n->mac.to_string() + "\n";
	for (int j = 0; j < n->nipv4; j++) {
		msg += "IPv4 address: " + n->ipv4[j].to_string();
		struct in_addr ip;
		// TODO - fill directly
		inet_pton(AF_INET, n->ipv4[j].to_string().c_str(), &ip);
		struct hostent *he = gethostbyaddr(&ip, sizeof(ip), AF_INET);
		if (he) msg += string(" (") + he->h_name + ")";
		msg += string(" last ") + ctime(&n->ipv4_ts[j]);
	}
	if (!n->lla.is_unspecified()) msg += "IPv6 LLA address: " + n->lla.to_string() + " last " + ctime(&n->lla_ts);
	for (int j = 0; j < n->naddrs; j++) {
		msg += "IPv6 address: " + n->addrs[j].to_string() + " last " + ctime(&n->addrs_ts[j]);
	}
	send_mail(event, n->mac, msg);
}

Node *new_node(const MacAddress &src_mac, const IPv6Address &src_ipv6, int errors)
{
	NCM::iterator mit = ncm.find(src_mac);
	Node *n,*pn;
	int i;
	time_t tm = time(NULL);

	if (mit == ncm.end()) {	// new node
		cerr << "NEW node mac " << src_mac << " IPv6 " << src_ipv6 << endl;
		n = static_cast<Node *>(operator new(sizeof(Node)));
		memset(n, 0, sizeof(Node));
		n->mac = src_mac;
		n->mac_ts = tm;
		ncm.insert(pair<MacAddress,Node *>(src_mac, n));
	} else {
		n = (*mit).second;
		n->mac_ts = tm;
	}
	NC6::iterator iit = nc6.find(src_ipv6);
	if (src_ipv6.is_unspecified()) {
		return n;
	}
	if (iit == nc6.end()) {	// not found
		pn = n;
	} else {
		pn = (*iit).second;
	}
	
	if (src_ipv6.is_linklocal()) {
		if (pn != n) {
			n->email &= ~UPD_NODE;
			// report new MAC old LLA? n->lla?
			cerr << "IPv6 LLA " << src_ipv6 << " changed MAC from " << pn->mac << " to " << src_mac << endl;
		}
		n->lla = src_ipv6;
		n->lla_ts = tm;
		if (pn != n || iit == nc6.end()) nc6[src_ipv6] = n;
	} else {
		bool found = false;
		for (i = 0; i < n->naddrs; i++) {
			if (n->addrs[i] == src_ipv6) found = true;
		}
		if (pn != n) {
			n->email &= ~UPD_NODE;
			cerr << "IPv6 addr " << src_ipv6 << " changed MAC from " << pn->mac << " to " << src_mac << endl;
			// clear NEW_NODE?
		}
		if (found) {
			n->addrs_ts[i] = tm;
		} else {
			if (n->naddrs >= MAX_ADDRS) {	// delete the oldest
				int tod = 0;
				for (i = 0; i < n->naddrs; i++) {
					if (n->addrs_ts[i] < n->addrs_ts[tod]) tod = i;
				}
				for (i = tod; i < n->naddrs-1; i++) {
					n->addrs[i] = n->addrs[i+1];
					n->addrs_ts[i] = n->addrs_ts[i+1];
				}
				n->naddrs--;
			}
			n->email &= ~UPD_NODE;
			n->addrs[n->naddrs] = src_ipv6;
			n->addrs_ts[n->naddrs] = tm;
			n->naddrs++;
		}
		if (pn != n || iit == nc6.end()) nc6[src_ipv6] = n;
	}
	if ((n->email & NEW_NODE) == 0 || (n->email & UPD_NODE) == 0) {
		send_upd_mail(((n->email & NEW_NODE) == 0)?NEW_NODE:UPD_NODE, n);
	}
	// write to db
	write_db(n);
	if (src_ipv6.is_linklocal()) {
		update_address(n, "lla", src_ipv6.to_string(), tm);
	} else {
		update_address(n, "gua", src_ipv6.to_string(), tm);
	}
	return n;
}

Node *mld_node(const MacAddress &src_mac, const IPv6Address &snd)
{
	NCM::iterator mit = ncm.find(src_mac);
	if (mit == ncm.end()) return NULL;	// we cannot add secondary address
	// TODO - store solicited-node group & ts
	Node *n = (*mit).second;
	return n;
}

void ipv4_node(const MacAddress &src_mac, const IPv4Address &src_ipv4)
{
	NCM::iterator mit = ncm.find(src_mac);
	Node *n;

	if (mit == ncm.end()) return;	// ignore ipv4 only
	time_t tm = time(NULL);
	n = (*mit).second;
	for (int j = 0; j < n->nipv4; j++) {
		if (n->ipv4[j] == src_ipv4) {
			n->ipv4_ts[j] = tm;
			update_address(n, "ipv4", src_ipv4.to_string(), tm);
			return;
		}
	}
	if (n->nipv4 >= MAX_ADDRS) {	// delete the oldest
		int tod = 0;
		int i;
		for (i = 0; i < n->nipv4; i++) {
			if (n->ipv4_ts[i] < n->ipv4_ts[tod]) tod = i;
		}
		for (i = tod; i < n->nipv4-1; i++) {
			n->ipv4[i] = n->ipv4[i+1];
			n->ipv4_ts[i] = n->ipv4_ts[i+1];
		}
		n->nipv4--;
	}
	n->ipv4[n->nipv4] = src_ipv4;
	n->ipv4_ts[n->nipv4] = tm;
	n->nipv4++;
	n->email &= ~UPD_NODE;		// send mail event
	send_upd_mail(UPD_NODE, n);
	update_address(n, "ipv4", src_ipv4.to_string(), tm);
}

void do_send(const string &email, const string &subj, const string &msg)
{
	// TODO: fork child process to send email without waiting
	// move gethostbyaddr to child process
	FILE *p = popen("/usr/sbin/sendmail -t", "w");
	if (!p) return;
	fprintf(p, "To: %s\n", email.c_str());
	fprintf(p, "From: ndwatch\n");
	fprintf(p, "Subject: %s\n", subj.c_str());
	fprintf(p, "\n");
	fprintf(p, "%s", msg.c_str());
	fprintf(p, "\n.\n");
	pclose(p);
}

void send_mail(unsigned event, const MacAddress &mac, const string &msg)
{
	NCM::iterator mit = ncm.find(mac);
	if (((*mit).second->email & event) != 0) return;
	EmailEvents::iterator it;
	for (it = email_events.begin(); it != email_events.end(); it++) {
		if (((*it).event & event) == 0) continue;
		// cerr << "send email " << msg << " to " << (*it).email << endl;
		string subj;
		switch (event) {
		case NEW_NODE: subj = "New node " + mac.to_string(); break;
		case UPD_NODE: subj = "Update node " + mac.to_string(); break;
		case INV_RA: subj = "Unknown router " + mac.to_string(); break;
		case INV_NA: subj = "Unknown router " + mac.to_string(); break;
		case DAD_DOS: subj = "DAD DoS " + mac.to_string(); break;
		case RA_RATE: subj = "RA rate " + mac.to_string(); break;
		}
		do_send((*it).email, subj, msg);
	}
	(*mit).second->email |= event;
	write_db((*mit).second);
}

// starting DAD, node is booting
// Vista/Windows 7
// LL-NS -> 
// RS -> 
// RA ->
// router-NS ->
// router-NA ->
// GA-NS -> 
//
// static address/router
// GA-NS ->  
// LL-NS ->
// router-NS -> router-NA
//
// DAD NS
void watch_init_ns(Node *n, const IPv6Address &src)
{
	if (src.is_linklocal()) {
		// DAD for LL address
//			cerr << "LLA DAD from invalid state" << endl;
		n->status &= ~(INIT_NSG|INIT_RS);
		n->status |= INIT_NSL;
	} else {	
		// DAD for global can start after RS/RA
		if ((n->status & (INIT_RS|DAD_RS_BAD)) == 0) {
			if (debug) cerr << "GA DAD without RS from " << n->mac << " for " << src << endl;
			n->status |= DAD_RS_BAD;
		}
		n->status |= INIT_NSG;
	}
}

// router NA
void watch_init_na(Node *n, const IPv6Address &src)
{
}

// after LL/GA DAD
void watch_init_rs(Node *n)
{
	if ((n->status &= INIT_NSL) == 0) {
		cerr << "Node " << n->mac << " RS without LL DAD" << endl;
	}
	n->status |= INIT_RS;
}

// RA to ff02::1
void watch_init_ra()
{
}

class Dnode {
public:
	MacAddress mac;
	IPv6Address ipv6;
	time_t ts;
	Dnode(const MacAddress &pmac, const IPv6Address &pipv6, time_t pts): mac(pmac), ipv6(pipv6), ts(pts) { }
};
typedef vector<Dnode *> DAD;
DAD dad;

//
// Duplicate Address Detection attack check
// 
void watch_dad(const MacAddress &src_mac, const IPv6Address &src_ipv6)
{
	DAD::iterator it;
	time_t ts = time(NULL);
	// already in queue?
	// NS may be repeated several times
	for (it = dad.begin(); it != dad.end(); ) {
		if ((*it)->mac == src_mac &&
		    (*it)->ipv6 == src_ipv6) {
			(*it)->ts = ts;
			// already started
			return;
		}
		// entry expired? 
		if (ts - (*it)->ts > 5) {	// max. DAD time = 3 * 1 sec.
			delete *it;
			it = dad.erase(it);
			continue;
		}
		it++;
	}
	if (debug) cerr << "Starting DAD watch for " << src_ipv6 << endl;
	Dnode *n = new Dnode(src_mac, src_ipv6, time(NULL));
	dad.push_back(n);
}

void watch_dad_dos(const MacAddress &src_mac, const IPv6Address &src_ipv6)
{
	DAD::iterator it;
	time_t ts = time(NULL);
	for (it = dad.begin(); it != dad.end(); ) {
		if ((*it)->ipv6 == src_ipv6 &&
		    (*it)->mac != src_mac) {
			if (ts - (*it)->ts < 5) {
				cerr << "DAD DoS target address " << src_ipv6 << " from " << src_mac << " to " << (*it)->mac << endl;
				send_mail(DAD_DOS, (*it)->mac, "Probable DAD DoS for " + src_ipv6.to_string() + " from mac " + (*it)->mac.to_string() + "\n");
			}
		}
		if (ts - (*it)->ts > 5) {	// max. DAD time = 3 * 1 sec.
			delete *it;
			it = dad.erase(it);
			continue;
		}
		it++;
	}
}

//
// purge bogus router - send RA with lifetime=0
//
void unregister_ra(const string &dev, const IPv6Address &src, ICMPv6ND_RA *nd_ra)
{
	Ethernet *e = new Ethernet(macs[dev], MacAddress("33:33:00:00:00:01"));
	IPv6 *ip = new IPv6(src, IPv6Address("ff02::1"));
	ip->hlim(255);
	e->attach(ip);
	ICMPv6ND_RA *nd = new ICMPv6ND_RA();
	nd->chlim(nd_ra->chlim());
	nd->raflags(nd_ra->raflags());
	nd->retrans(nd_ra->retrans());
	nd->rltime(0);
	// no prefixes?
	ip->attach(nd);
	
	unsigned delay = random();
	delay = (delay % (MAX_DELAY - MIN_DELAY)) + MIN_DELAY;
	unsigned long long when = get_time() + delay;
	// add to queue
	while (purge_queue.find(when) != purge_queue.end()) when++;
	struct PQE pqe;
	pqe.p = e;
	pqe.dev = &dev;
	purge_queue[when] = pqe;
	// cerr << "Purge router " << when << endl;
}

void do_purge(const string *dev, Packet *p)
{
	Buffer pkt;
	cerr << "Purging bogus router " << dynamic_cast<IPv6 *>(p->payload())->src() << endl;
	if (!p->build(pkt)) {
		cerr << "Cannot build purge RA packet" << endl;
		return;
	}
	pcap.write(*dev, pkt);
}

//
// Check Neighbor Solicitation Message
//
// RFC 4861 7.1.1.
// a - ICMP Checksum is valid.
// b - ICMP Code is 0.
// c - ICMP length (derived from the IP length) is 24 or more octets.
//     = handled in decode (decode fail)
// d - Target Address is not a multicast address.
// e - All included options have a length that is greater than zero.
//     = handled in decode (decode fail)
// f - If the IP source address is the unspecified address, the IP
//     destination address is a solicited-node multicast address.
// g - If the IP source address is the unspecified address, there is no
//     source link-layer address option in the message.
// 7.2.2
// h - If the solicitation is being sent to a solicited-node multicast
//     address, the sender MUST include its link-layer address (if it has
//     one) as a Source Link-Layer Address option.
//
void check_ns(const string &dev, Ethernet *eh, IPv6 *ipv6, ICMPv6ND_NS *nd_ns)
{
	int errors = 0;

	// check target address (d)
	if (nd_ns->target().is_multicast()) {
		cerr << "ND NS target address is multicast address " << nd_ns->target() << endl;
		errors++;
	} else // check subnet prefix
	if (!nd_ns->target().is_linklocal() && !ipv6->src().is_linklocal() &&
	    !ipv6->src().is_unspecified()) {
		if (!nd_ns->target().is_prefix(ipv6->src(), 64)) {
			cerr << "ND NS target address " << nd_ns->target() << " is not in source address prefix " << ipv6->src() << endl;
			errors++;
		}
	}
	// DAD NS (f)
	if (ipv6->src().is_unspecified()) {
		if (!ipv6->dest().is_multicast()) {
			cerr << "ND NS DAD invalid dst address (should be solicited-node multicast) " << ipv6->dest() << endl;
			errors++;
		}
	}
	if (ipv6->dest().is_multicast()) {
		// check dst ff02::1:ffXX:YYZZ / target
		if (ipv6->dest().multicast_scope() != IPv6Address::link_local) {
			cerr << "ND NS invalid dst solicited-node multicast scope " << static_cast<int>(ipv6->dest().multicast_scope()) << endl;
			errors++;
		}
		if (ipv6->dest().multicast_flags() != 0) {
			cerr << "ND NS invalid dst solicited-node multicast flags " << ipv6->dest().multicast_flags() << endl;
			errors++;
		}
		if (!ipv6->dest().is_prefix(IPv6Address("ff02::1:ff00:0"), 104)) {
			cerr << "ND NS invalid dst solicited-node multicast prefix " << ipv6->dest() << endl;
			errors++;
		}
	} else {
		if (ipv6->dest() != nd_ns->target()) {
			cerr << "ND NS target address " << nd_ns->target() << " is not equal to dest address " << ipv6->dest() << endl;
			errors++;

		}
	}

	// check options
	int i = 0;
	int sourcella = 0;
	while (nd_ns->get_option(i)) {
		if (dynamic_cast<NDOptionSourceLLA *>(nd_ns->get_option(i)) != NULL) {
			NDOptionSourceLLA *slla = dynamic_cast<NDOptionSourceLLA *>(nd_ns->get_option(i));
			sourcella++;
			// DAD NS cannot contain SLLA (g)
			if (ipv6->src().is_unspecified() && sourcella) {
				cerr << "ND NS DAD containing SourceLLA option " << slla->lla() << endl;
				errors++;
			}
			if (slla->lla() != eh->src()) {
				cerr << "ND NS mac address mismatch in SourceLLA option " << slla->lla() << " mac src " << eh->src() << endl;
				errors++;
			}
		}
		i++;
	}	
	// Check missing option (h)
	// typical for Windows XP, cannot be checked
	if (false && ipv6->dest().is_multicast() && sourcella == 0) {
		cerr << "ND NS solicited-node multicast without Source LLA options" << endl;
		errors++;
	}
	if (sourcella > 1) {
		cerr << "ND NS multiple Source LLA options" << endl;
		errors++;
	} 

	if (errors == 0 && debug) {
		cerr << "Valid ND NS";
		if (ipv6->dest().is_prefix(IPv6Address("ff02::1:ff00:0"), 104)) cerr << "-SN";
		cerr << " from " << eh->src() << " target " << nd_ns->target() << endl;
	}
	Node *n = new_node(eh->src(), ipv6->src(), errors);

	// DAD
	if (ipv6->src().is_unspecified() &&
	    ipv6->dest().is_prefix(IPv6Address("ff02::1:ff00:0"), 104)) {
		if (nd_ns->target().is_linklocal()) {
			time_t diff = time(NULL) - n->lla_mld_ts;
			if (diff > 5 && (n->status & MLD_BAD) == 0) {		// arbitrary low limit
				if (debug) cerr << "ND NS-SN without previous MLD Report for solicited-node group target " << nd_ns->target() << endl;
				n->status |= MLD_BAD;	// don't report again
			}
		}
		watch_dad(eh->src(), nd_ns->target());
		watch_init_ns(n, nd_ns->target());
	}
}

//
// Check Neighbor Advertisement message
//
// RFC 4861 7.1.2 Validation of Neighbor Advertisements
// - The IP Hop Limit field has a value of 255 (main loop)
// - ICMP Checksum is valid (checked in decode)
// - ICMP Code is 0 (TODO)
// - ICMP length (derived from the IP length) is 24 or more octets (decode)
// - Target Address is not a multicast address (here)
// - If the IP Destination Address is a multicast address the Solicited flag is zero (here)
// - All included options have a length that is greater than zero (decode)
// - The only defined option that may appear is the Target Link-Layer Address option (here)
// 7.2.4. Sending Solicited Neighbor Advertisement
// - If the solicitation's IP Destination Address is not a multicast address, 
//   the Target Link-Layer Address option MAY be omitted
// - If the solicitation's IP Destination Address is a multicast address, the
//   Target Link-Layer option MUST be included in the advertisement
// - if the node is a router, it MUST set the Router flag to
//   one; otherwise, it MUST set the flag to zero.
// - If the Target Address is either an anycast address or a unicast
//   address for which the node is providing proxy service, or the Target
//   Link-Layer Address option is not included, the Override flag SHOULD
//   be set to zero. 
// - Otherwise, the Override flag SHOULD be set to one.
// - If the source of the solicitation is the unspecified address, the
//   node MUST set the Solicited flag to zero and multicast the
//   advertisement to the all-nodes address.
// - Otherwise, the node MUST set the Solicited flag to one and unicast 
//   the advertisement to the Source Address of the solicitation.
// 7.2.6. Sending Unsolicited Neighbor Advertisements
// - a node MAY send up to MAX_NEIGHBOR_ADVERTISEMENT unsolicited Neighbor 
//   Advertisement messages to the all-nodes multicast address.
// - The Target Address field in the unsolicited advertisement is set to
//   an IP address of the interface, and the Target Link-Layer Address
//   option is filled with the new link-layer address.
// - The Solicited flag MUST be set to zero
// - A node that has multiple IP addresses assigned to an interface MAY
//   multicast a separate Neighbor Advertisement for each address.
//
void check_na(const string &dev, Ethernet *eh, IPv6 *ipv6, ICMPv6ND_NA *nd_na)
{
	int errors = 0;

	// check destination address
	if (ipv6->dest().is_multicast()) {
		if (nd_na->flags() & ICMPv6ND_NA::Solicited) {	// S flag
			cerr << "ND NA destination address " << nd_na->target() << " is a multicast address and S flag is not zero " << endl;
			errors++;
		}
		// multicast destination must be all-nodes
		if (ipv6->dest() != IPv6Address("ff02::1")) {
			cerr << "ND NA destination address is unexpected multicat address " << ipv6->dest() << endl;
			errors++;
		}
	} else {
		// unsolicited NA destination cannot be unicast address
		if ((nd_na->flags() & ICMPv6ND_NA::Solicited) == 0) {	// S flag
			cerr << "ND NA destination address " << nd_na->target() << " is a unicast address and S flag is zero " << endl;
			errors++;
		}
	}

	// check target address
	if (nd_na->target().is_multicast()) {
		cerr << "ND NA target address is multicast address " << nd_na->target() << endl;
		errors++;
	}

	// check target address prefix
	if (!ipv6->dest().is_multicast() && !ipv6->dest().is_linklocal() && !nd_na->target().is_linklocal()) {
		if (!nd_na->target().is_prefix(ipv6->dest(), 64)) {
			cerr << "ND NA target address " << nd_na->target() << " is not in IPv6 dst subnet prefix " << ipv6->dest() << endl;
			errors++;
		}
	}

	// check target address corresponding to ipv6 source address
	if (ipv6->src() != nd_na->target()) {
		// valid for multiple global addresses (temporary, virtual host)
		//if (ipv6->src().is_linklocal() && !nd_na->target().is_linklocal() ||
		//   !ipv6->src().is_linklocal() && nd_na->target().is_linklocal() ||
		if (ipv6->src().is_linklocal() && nd_na->target().is_linklocal()) {
			cerr << "ND NA target address " << nd_na->target() << " is not equal to source address " << ipv6->src() << endl;
			errors++;
		}
	}

	// The only option that may appear is the Target Link-Layer Address
	int i = 0;
	int targetlla = 0;
	while (nd_na->get_option(i)) {
		NDOptionTargetLLA *tlla;
		if ((tlla = dynamic_cast<NDOptionTargetLLA *>(nd_na->get_option(i))) != NULL) {
			if (tlla->lla() != eh->src()) {
				cerr << "ND NA mac address mismatch in TargetLLA option " << tlla->lla() << " mac src " << eh->src() << endl;
				errors++;
			}
			targetlla++;
		} else {
			cerr << "Invalid option " << nd_na->get_option(i)->name() << endl;
			errors++;
		}
		i++;
	}
	if (targetlla > 1) {
		cerr << "ND NA multiple Target LLA options" << endl;
		errors++;
	} else
	if ((nd_na->flags() & ICMPv6ND_NA::Solicited) == 0) {	// not solicited
		if (targetlla != 1) {
			cerr << "Unsolicited ND NA should contain Target LLA option" << endl;
			errors++;
		}
	}

	// check if NA has router flag
	// check if NA is official router from list
	if (nd_na->flags() & ICMPv6ND_NA::Router) {	// R flag
		// get subnet
		Subnets::iterator it = subnets.begin();
		bool found = false;
		while (it != subnets.end()) {
			if (eh->src() == (*it).mac) {
				found = true;
				break;
			}
			it++;
		}
		if (!found) {
			cerr << "ND NA from " << ipv6->src() << " with router flag, but source mac " << eh->src() << " not in our router list" << endl;
			send_mail(INV_NA, eh->src(), "Invalid NA with router flag from " + ipv6->src().to_string() + " mac " + eh->src().to_string() + "\n");
			errors++;
		} else {
			found = false;
			for (int i = 0; i < (*it).naddrs; i++) {
				if ((*it).addrs[i] == ipv6->src()) found = true;
			}
			if (!found) {
				cerr << "ND NA with router flag, but source address " << ipv6->src() << " not in our router list" << endl;
				send_mail(INV_NA, eh->src(), "Invalid NA with router flag from " + ipv6->src().to_string() + " mac " + eh->src().to_string() + "\n");
				errors++;
			} 
		}
	}
	
	if (nd_na->flags() & ICMPv6ND_NA::Router) {	// R flag
		if (errors == 0 && debug) cerr << "Valid ND NA router from " << eh->src() << " target " << nd_na->target() << endl;
	} else {
		if (errors == 0 && debug) cerr << "Valid ND NA host from " << eh->src() << " target " << nd_na->target() << endl;
	}
	Node *n = new_node(eh->src(), ipv6->src(), errors);

	// check previous MLD report time
	if (nd_na->target().is_linklocal()) {
		Subnets::iterator it = subnets.begin();
		bool found = false;
		while (it != subnets.end()) {
			if (dev == (*it).dev) {
				found = true;
				break;
			}
			it++;
		}
		time_t now = time(NULL);
		if (found && (*it).last_querier &&
		    now - (*it).last_querier < MLD_QUERY_INTERVAL*2 &&
		    now - n->lla_mld_ts > MLD_QUERY_INTERVAL*2 && 
		    (n->status & MLD_BAD) == 0) {
			if (now - start_time > MLD_QUERY_INTERVAL*2) {
				cerr << "ND NA from " << eh->src() << " without previous MLD Report for solicited-node group " << nd_na->target();
				if (n->lla_mld_ts) cerr << " diff " << (now - n->lla_mld_ts);
				cerr << endl;
				n->status |= MLD_BAD;	// don't report again
			}
		}
	} else {	// TODO

	}
	watch_dad_dos(eh->src(), nd_na->target());
}

//
// Check router solicitation message
// RFC4861 6.1.1. Validation of Router Solicitation Messages
// - The IP Hop Limit field has a value of 255, i.e., the packet
//   could not possibly have been forwarded by a router.
// - ICMP Checksum is valid.
// - ICMP Code is 0.
// - ICMP length (derived from the IP length) is 8 or more octets.
// - All included options have a length that is greater than zero.
// - If the IP source address is the unspecified address, there is no
//   source link-layer address option in the message.
// - The only defined option that may appear is the Source Link-Layer 
//   Address option.
//
void check_rs(const string &dev, Ethernet *eh, IPv6 *ipv6, ICMPv6ND_RS *nd_rs)
{
	int errors = 0;

	// check options
	int i = 0;
	int sourcella = 0;
	while (nd_rs->get_option(i)) {
		if (dynamic_cast<NDOptionSourceLLA *>(nd_rs->get_option(i)) != NULL) {
			NDOptionSourceLLA *slla = dynamic_cast<NDOptionSourceLLA *>(nd_rs->get_option(i));
			sourcella++;
			// RS with unspec source cannot contain SLLA (g)
			if (ipv6->src().is_unspecified() && sourcella == 1) {
				cerr << "ND RS with unspecified source address containing SourceLLA option " << slla->lla() << " from " << eh->src() << endl;
				errors++;
			}
			if (slla->lla() != eh->src()) {
				cerr << "ND RS mac address mismatch in SourceLLA option " << slla->lla() << " from " << eh->src() << endl;
				errors++;
			}
		}
		i++;
	}	
	if (sourcella > 1) {
		cerr << "ND RS multiple Source LLA options from " << eh->src() << endl;
		errors++;
	} 
	if (errors == 0 && debug) cerr << "Valid ND RS from " << eh->src() << " " << ipv6->src() << endl;

	new_node(eh->src(), ipv6->src(), errors);
}

//
// Check Router Advertisement Message
// - IP Source Address is a link-local address.  Routers must use
//   their link-local address as the source for Router Advertisement
//   and Redirect messages so that hosts can uniquely identify routers.
// - The IP Hop Limit field has a value of 255, i.e., the packet
//   could not possibly have been forwarded by a router.
// - ICMP Checksum is valid.
// - ICMP Code is 0.
// - ICMP length (derived from the IP length) is 16 or more octets.
// - All included options have a length that is greater than zero.
// - The only defined options that may appear are
//   the Source Link-Layer Address, Prefix Information and MTU options.
// 6.2.6. Processing Router Solicitations
// - A router MAY choose to unicast the response directly to the soliciting 
//   host's address (if the solicitation's source address is not the 
//   unspecified address), but the usual case is to multicast the response 
//   to the all-nodes group.
// - Consecutive Router Advertisements sent to the all-nodes multicast address 
//   MUST be rate limited to no more than one advertisement every 
//   MIN_DELAY_BETWEEN_RAS seconds.
// 6.2.7. Router Advertisement Consistency (sent by other routers)
// - Cur Hop Limit values (except for the unspecified value of zero
//   other inconsistencies SHOULD be logged to system network management).
// - Values of the M or O flags.
// - Reachable Time values (except for the unspecified value of zero).
// - Retrans Timer values (except for the unspecified value of zero).
// - Values in the MTU options.
// - Preferred and Valid Lifetimes for the same prefix.
// 6.2.8. Link-local Address Change
// - The link-local address on a router should rarely change, if ever.
//
void check_ra(const string &dev, Ethernet *eh, IPv6 *ipv6, ICMPv6ND_RA *nd_ra)
{
	int errors = 0;
	bool bogus_router = false;
	string msg;

	// check dest
	if (ipv6->dest().is_multicast()) {
		if (ipv6->dest() != IPv6Address("ff02::1")) {
			cerr << "ND RA invalid multicast destination address " << ipv6->dest() << " from " << eh->src() << endl;
			errors++;
		}
	}
	// check router source lla 
	Subnets::iterator it = subnets.begin();
	bool found = false;
	while (it != subnets.end()) {
		if (ipv6->src() == (*it).addrs[0]) {
			found = true;
			break;
		}
		it++;
	}
	if (!found) {
		if (nd_ra->rltime()) {	// ignore purge packet
			cerr << "ND RA undefined router with LLA " << ipv6->src() << " from " << eh->src() << endl;
			bogus_router = true;
			msg += "Undefined router LLA " + ipv6->src().to_string() + "\n";
			errors++;
		}
	} else
	if ((*it).mac != eh->src()) {
		cerr << "ND RA unknown mac address " << eh->src() << " for LLA " << ipv6->src() << endl;
		errors++;
	}
	int i = 0;
	int sourcella = 0;
	int mtu = 0;
	string spi;
	while (nd_ra->get_option(i)) {
		NDOptionSourceLLA *slla;
		NDOptionPrefixInfo *pi;
		NDOptionMTU *omtu;
		NDOptionRouteInfo *ri;
		if ((slla = dynamic_cast<NDOptionSourceLLA *>(nd_ra->get_option(i))) != NULL) {
			if (slla->lla() != eh->src()) {
				cerr << "ND RA mac address mismatch in SourceLLA option " << slla->lla() << " from " << eh->src() << endl;
				errors++;
			}
			sourcella++;
		} else 
		if ((omtu = dynamic_cast<NDOptionMTU *>(nd_ra->get_option(i))) != NULL) {
			if (omtu->mtu() < 1280 || omtu->mtu() > 9200) {
				cerr << "ND RA invalid MTU " << omtu->mtu() << " from " << eh->src() << endl;
				errors++;
			} 
		} else
		if ((pi = dynamic_cast<NDOptionPrefixInfo *>(nd_ra->get_option(i))) != NULL) {
			bool foundr = false;
			bool foundp = false;
			for (it = subnets.begin(); it != subnets.end(); it++) {
				// TODO match dev
				if ((*it).prefix == pi->prefix() &&
			    	    (*it).plen == pi->plen()) {
					foundp = true;
					if (ipv6->src() == (*it).addrs[0]) {
						foundr = true;
						break;
					}
				}
			}
			if (!foundp) {
				if (nd_ra->rltime()) {  // ignore purge
					cerr << "ND RA unknown prefix " << pi->prefix() << "/" << (int)pi->plen() << " from " << ipv6->src() << " " << eh->src() << endl;
					bogus_router = true;
					msg += "Unknown prefix " + pi->prefix().to_string() + "/" + cvt_int(pi->plen()) + "\n";
					errors++;
				}
			} else 
			if (!foundr) {
				if (nd_ra->rltime()) {  // ignore purge
					cerr << "ND RA prefix " << pi->prefix() << "/" << (int)pi->plen() << " from uknown router " << ipv6->src() << " " << eh->src() << endl;
					bogus_router = true;
					msg += "Unknown router for " + pi->prefix().to_string() + "/" + cvt_int(pi->plen()) + "\n";
					errors++;
				}
			} else {
				spi = pi->prefix().to_string() + "/" + cvt_int(pi->plen()) + " ";
			}
			// check Prefix Info validity
			if (!pi->is_auto()) {	// TODO optional
				cerr << "ND RA Prefix Info option without A flag " << pi->prefix() << "/" << (int)pi->plen() << " from " << eh->src() << endl;
				errors++;
			}
			if (pi->preferred_lifetime() > pi->valid_lifetime()) {
				cerr << "ND RA Prefix Info option with Pref>Valid lifetime " << pi->prefix() << "/" << (int)pi->plen() << " from " << eh->src() << endl;
				errors++;
			}
		} else 	// typical for Windows 7 ICS
		if ((ri = dynamic_cast<NDOptionRouteInfo *>(nd_ra->get_option(i))) != NULL) {
			// XXX check route info
		} else {
			cerr << "Invalid option " << nd_ra->get_option(i)->name() << " from " << eh->src() << endl;
			errors++;
		}
		i++;
	}
	if (sourcella > 1) {
		cerr << "ND NA multiple Source LLA options from " << eh->src() << endl;
		errors++;
	} 
	if (mtu > 1) {
		cerr << "ND NA multiple MTU options from " << eh->src()  << endl;
		errors++;
	} 
	if (errors == 0 && debug) cerr << "Valid ND RA from " << eh->src() << " " << ipv6->src() << " prefix " << spi << endl;

	if (nd_ra->rltime()) {	// ignore purge packets
		new_node(eh->src(), ipv6->src(), errors);
	} 
	
	if (bogus_router && nd_ra->rltime()) {
		send_mail(INV_RA, eh->src(), "Unknown router with mac " + eh->src().to_string() + "\n" + msg);
		if (uflag) unregister_ra(dev, ipv6->src(), nd_ra);
	}
}

void check_arp(const string &dev, Ethernet *eh, ARP *arp)
{
	IPv4Address spa;
	MacAddress sha;
	if (arp->operation() == ARP::REQUEST) {
		arp->get_sender(sha, spa);
		if (sha == eh->src() && !spa.is_unspecified()) ipv4_node(sha, spa);
	} else {
		arp->get_sender(sha, spa);
		if (sha == eh->src() && !spa.is_unspecified()) ipv4_node(sha, spa);
	}
}

void check_mld(const string &dev, Ethernet *e, IPv6 *ipv6, const IPv6Address &group, bool v2)
{
	Node *n;
	int errors = 0;
	if (ipv6->hlim() != 1) {
		cerr << " MLD wrong IPv6 hop limit " << ipv6->hlim() << " != 1" << endl;
		errors++;
	}
	if (!group.is_multicast()) {
		cerr << "MLD invalid dst multicast group " << group << endl;
		errors++;
	}
	if (!ipv6->src().is_linklocal() && !ipv6->src().is_unspecified()) {
		cerr << "MLD invalid non-linklocal source address " << ipv6->src() << endl;
		errors++;
	}
	if (ipv6->dest().is_prefix(IPv6Address("ff02::1:ff00:0"), 104)) {
		if (ipv6->dest().multicast_scope() != IPv6Address::link_local) {
			cerr << "MLD invalid dst solicited-node multicast scope " << static_cast<int>(ipv6->dest().multicast_scope()) << endl;
			errors++;
		}
		if (ipv6->dest().multicast_flags() != 0) {
			cerr << "MLD invalid dst multicast flags " << ipv6->dest().multicast_flags() << endl;
			errors++;
		}
		if (v2) {
			if (ipv6->dest() != IPv6Address("ff02::16")) {
				cerr << "MLDv2 invalid dst address " << ipv6->dest() << " for solicited-node group " << group << endl;
				errors++;
			}
		} else {
			if (ipv6->dest() != group) {
				cerr << "MLD invalid dst address " << ipv6->dest() << " for solicited-node group" << group << endl;
				errors++;
			}
		}
		IPv6Address sg;
		if (ipv6->src().is_unspecified()) {	// booting
			n = new_node(e->src(), ipv6->src(), errors);
			if (!n->lla.is_unspecified()) {
				sg.solicited_multicast(n->lla);
				if (sg == group) {	// lla address
					n->lla_mld_ts = time(NULL);
				} else {
					// TODO GUA address
				}
			} else {	// new node
				// TODO store solicited mcast addr for mac node
				// n->slla = group;
			}
		} else {
			sg.solicited_multicast(ipv6->src());
			if (sg == group) {	// primary address
				n = new_node(e->src(), ipv6->src(), errors);
				if (ipv6->src().is_linklocal()) {
					n->lla_mld_ts = time(NULL);
				} else {
					// cannot happen
				}
			} else {
				n = mld_node(e->src(), ipv6->src());
			}
		}
	}
}

void handle_sig(int sig)
{
	finish = true;
}

void handle_info(int sig)
{
	dump_stat = true;
}

int main(int argc, char *argv[])
{

	Ethernet *e = new Ethernet();
	Buffer buf;
	bool pcap_open = false;
	debug = 0;
	MacAddress mac;
	start_time = time(NULL);
	Stats *ps;
	TStats::iterator its;

	int c;
	opterr = 0;
	while ((c = getopt(argc, argv, "dtui:")) != -1) {
		switch (c) {
		case 'd': debug++; break;
		case 'i': 
			  if (!if_get_mac(optarg, mac)) {
				  cerr << "get_if_mac: cannot get mac address for " << optarg << endl;
				  return 1;
			  }
			  macs[optarg] = mac;
			  if (!pcap.open(optarg)) {
				  cerr << "pcap_open: cannot open " << optarg << endl;
				  return 1;
			  }
			  pcap_open = true;
			  break;
		case 't': dflag = true; debug = 1; break;
		case 'u': uflag = true; break;
		case '?':
			  cerr << "Unknown option " << static_cast<char>(optopt) << endl;
			  break;
		default:
			cerr << "ndwatch [-dut][-i if]..." << endl;
		}
	}

	struct sigaction sig;
	sig.sa_handler = handle_sig;
	sig.sa_flags = 0;
	sigemptyset(&sig.sa_mask);
	sigaction(SIGINT, &sig, NULL);
	sig.sa_handler = handle_info;
	sigaction(SIGINFO, &sig, NULL);

	read_config();
	if (dflag) return 0;

	if (!pcap_open && !pcap.open()) {
		cerr << "cannot open pcap device" << endl;
		return 1;
	}
	string dev;
	// MLD contains HBH header and so it's not matched with icmp6 rule
	pcap.set_filter("icmp6 or ip6 proto 0 or arp");
	while (!finish) {
		if (dump_stat) {
			for (its = stats.begin(); its != stats.end(); its++) {
				cerr << "interface " << (*its).first << endl;
				ps = (*its).second;
				for (int i = 0; i < 255; i++) {
					if (ps->count(i) == 0) continue;
					cerr << ps->to_string(i) << endl;
				}
			}
			dump_stat = false;
		}
		long long delay = 0;
		PurgeQueue::iterator it = purge_queue.begin();
		while (it != purge_queue.end()) {
			delay = (*it).first - get_time();
			// cerr << "Purging " << delay << endl;
			if (delay <= 0) {
				do_purge((*it).second.dev, (*it).second.p);
				delete (*it).second.p;
				purge_queue.erase(it);
				it = purge_queue.begin();
				delay = 0;
				continue;
			}
			delay += 10;	// timer inaccuracy
			break;
		}
		if (delay == 0 && ra_rate_time) {
			delay = 1000;
		}
		// get packet
		if (!pcap.read(buf, dev, delay)) continue;
		cur_time = 0;

		// send delayed rate limit message
		if (ra_rate_time) {
			cur_time = time(NULL);
			// if the same sec, continue counting
			if (cur_time != ra_rate_time) {
				ra_last_email = cur_time;
				cerr << ra_rate_msg << endl;
				send_mail(RA_RATE, e->src(), ra_rate_msg);
				ra_rate_time = 0;
			}
		}
		// full decode
		if (!e->decode(buf)) continue;
		// is any decoded payload?
		if (!e->payload()) continue;
		if (dynamic_cast<ARP *>(e->payload()) != NULL) {
			if ((its = stats.find(dev)) == stats.end()) stats[dev] = new Stats;
			ps = (*its).second;
			if (cur_time == 0) cur_time = time(NULL);
			ps->update("ARP", ARP_STAT, cur_time);
			check_arp(dev, e, dynamic_cast<ARP *>(e->payload()));
			continue;
		}
		// is IPv6 packet?
		if (dynamic_cast<IPv6 *>(e->payload()) == NULL) continue;
		IPv6 *ipv6 = dynamic_cast<IPv6 *>(e->payload());
		// is fragment?
		if (ipv6->is_fragment()) {
			// fragment is not decoded, no verification
			if (ipv6->get_last_nh() == ICMPv6::ND_RS ||
			    ipv6->get_last_nh() == ICMPv6::ND_RA ||
			    ipv6->get_last_nh() == ICMPv6::ND_NA ||
			    ipv6->get_last_nh() == ICMPv6::ND_NS ||
			    ipv6->get_last_nh() == ICMPv6::ND_Redirect ||
			    ipv6->get_last_nh() == ICMPv6::MLD_Query ||
			    ipv6->get_last_nh() == ICMPv6::MLD_Report ||
			    ipv6->get_last_nh() == ICMPv6::MLD_Done) {
				if (debug > 1) {
					cout << endl << "NEW PACKET:" << endl;
					cout << e << endl;
					cout << ipv6 << endl;
				}
				cerr << "Invalid fragmented ND/MLD packet from " <<  e->src() << endl;
			}
			continue;
		}
		// is any IPv6 data?
		if (!ipv6->payload()) continue;
		// is ICMPv6 data?
		if (dynamic_cast<ICMPv6 *>(ipv6->payload()) == NULL) continue;
		ICMPv6 *icmpv6 = dynamic_cast<ICMPv6 *>(ipv6->payload());
		// is MLD/ND packet?
		if (icmpv6->type() != ICMPv6::MLD_Query &&
		    icmpv6->type() != ICMPv6::MLD_Report &&
		    icmpv6->type() != ICMPv6::MLD_Done &&
		    icmpv6->type() != ICMPv6::MLDv2_Report &&
		    icmpv6->type() != ICMPv6::ND_RS &&
		    icmpv6->type() != ICMPv6::ND_RA &&
		    icmpv6->type() != ICMPv6::ND_NS &&
		    icmpv6->type() != ICMPv6::ND_NA &&
		    icmpv6->type() != ICMPv6::ND_Redirect) continue;

		// debug dump
		if (debug > 1) {
			cout << endl << "NEW PACKET:" << endl;
			cout << e << endl;
			cout << ipv6 << endl;
			cout << icmpv6 << endl;
		}

		// update statistics
		if ((its = stats.find(dev)) == stats.end()) stats[dev] = new Stats;
		ps = (*its).second;
		if (cur_time == 0) cur_time = time(NULL);
		long cur1s = ps->update(icmpv6->name(), icmpv6->type(), cur_time);

		// check RA rate limit
		if (icmpv6->type() == ICMPv6::ND_RA && cur1s > ra_limit) {
			if (ra_last_email - cur_time > ra_email) {
				ra_rate_time = cur_time;
				ra_rate_msg = "RA rate exceeded (";
				ra_rate_msg += cvt_int(cur1s);
				ra_rate_msg += ") from " + e->src().to_string();
			}
		}
		//
		// general checks
		//
		// check eth broadcast
		if (e->src().is_broadcast()) {
			cerr << icmpv6->name() << " source MAC is broadcast" << endl;
			continue;
		}
		if (e->src().is_unspecified()) {
			cerr << icmpv6->name() << " source MAC is unspecified" << endl;
			continue;
		}

		// check ip source for multicast address
		if (ipv6->src().is_multicast()) {
			cerr << icmpv6->name() << " source IPv6 address is multicast address " << ipv6->src() << endl;
			continue;
		} else // check ip source prefix
		if (!ipv6->src().is_linklocal() &&
		    !ipv6->src().is_sitelocal() &&
		    !ipv6->src().is_unspecified()) {
			bool found = false;
			for (Subnets::iterator it = subnets.begin(); it != subnets.end(); it++) {
			    if (ipv6->src().is_prefix((*it).prefix, (*it).plen)) {
				if ((*it).dev != dev) {
					cerr << icmpv6->name() << " unexpected IPv6 source address prefix " << ipv6->src() << " from " << dev << endl;
				}
				found = true;
				break;
			    }
			}
			if (!found) {	// TODO add mail event
				cerr << icmpv6->name() << " unexpected IPv6 source address prefix " << ipv6->src() << " from " << dev << endl;
			}
		}

		// MLD processing
		if (icmpv6->type() == ICMPv6::MLD_Query) {
			IPv6Address group;
			if (dynamic_cast<ICMPv6MLD_Query *>(icmpv6)) {
				ICMPv6MLD_Query *mld = dynamic_cast<ICMPv6MLD_Query *>(icmpv6);
				group = mld->group();
			} else {
				ICMPv6MLDv2_Query *mld = dynamic_cast<ICMPv6MLDv2_Query *>(icmpv6);
				group = mld->group();
			}
			// MLD Querier?
			if (group.is_unspecified() &&
			    ipv6->dest() == IPv6Address("ff02::1")) {
				// TODO - store address?
				// querier stored only in the first occurence of dev
				for (Subnets::iterator it = subnets.begin(); it != subnets.end(); it++) {
					if (dev != (*it).dev) continue;
					if (!(*it).last_querier) cerr << "Detected MLD Querier for " << (*it).prefix << "/" << (*it).plen << " at MAC " << e->src() << " LLA " << ipv6->src() << endl;
					(*it).last_querier = time(NULL);
					break;
				}
			}
			continue;
		} else
		if (icmpv6->type() == ICMPv6::MLD_Done) {
			continue;
		} else
		if (icmpv6->type() == ICMPv6::MLD_Report) {
			ICMPv6MLD_Report *mld = dynamic_cast<ICMPv6MLD_Report *>(icmpv6);
			check_mld(dev, e, ipv6, mld->group(), false);
			continue;
		} else
		if (icmpv6->type() == ICMPv6::MLDv2_Report) {
			ICMPv6MLDv2_Report *mld = dynamic_cast<ICMPv6MLDv2_Report *>(icmpv6);
			IPv6Address group;
			int i = 0;
			while (mld->get_group(i, group)) {
				check_mld(dev, e, ipv6, group, true);
				i++;
			}
			continue;
		}

		// check hlim
		if (ipv6->hlim() != 255) {
			cerr << icmpv6->name() << " wrong IPv6 hop limit " << ipv6->hlim() << " != 255 " << endl;
		}

		switch (icmpv6->type()) {
		case ICMPv6::ND_RS:
			check_rs(dev, e, ipv6, dynamic_cast<ICMPv6ND_RS *>(icmpv6));
			break;
		case ICMPv6::ND_RA:
			check_ra(dev, e, ipv6, dynamic_cast<ICMPv6ND_RA *>(icmpv6));
			break;
		case ICMPv6::ND_NS:
			check_ns(dev, e, ipv6, dynamic_cast<ICMPv6ND_NS *>(icmpv6));
			break;
		case ICMPv6::ND_NA:
			check_na(dev, e, ipv6, dynamic_cast<ICMPv6ND_NA *>(icmpv6));
			break;
		case ICMPv6::ND_Redirect:
			// TODO watch and mail
			break;
		}
	}
	close_db();
	return 0;
}
