/* SPDX-License-Identifier: GPL-2.0-only
 *
 * dlmod plugin for net-snmp for monitoring
 * ipt_NETFLOW module via IPT-NETFLOW-MIB.
 *
 * (c) 2014 <abc@telekom.ru>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>

#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>

#define iptNetflowMIB_oid 1, 3, 6, 1, 4, 1, 37476, 9000, 10, 1 /* .1.3.6.1.4.1.37476.9000.10.1 */

/* iptNetflowObjects */
static oid iptNetflowModule_oid[]    = { iptNetflowMIB_oid, 1, 1 };
static oid iptNetflowSysctl_oid[]    = { iptNetflowMIB_oid, 1, 2 };
/* iptNetflowStatistics */
static oid iptNetflowTotals_oid[]    = { iptNetflowMIB_oid, 2, 1 };
static oid iptNetflowCpuTable_oid[]  = { iptNetflowMIB_oid, 2, 2 };
static oid iptNetflowSockTable_oid[] = { iptNetflowMIB_oid, 2, 3 };

struct snmp_vars {
	int obj;
	int type;
	char *name;

	time_t ts; /* when value last read */
	long long val64;
};

struct snmp_vars modinfos[] = {
	{1, ASN_OCTET_STR, "name"},
	{2, ASN_OCTET_STR, "version"},
	{3, ASN_OCTET_STR, "srcversion"},
	{4, ASN_OCTET_STR, "loadTime"}, /* DateAndTime */
	{5, ASN_INTEGER,   "refcnt"},
	{ 0 }
};
#define MODINFO_NAME "ipt_NETFLOW"
#define MODINFO_NAME_ID 1
#define MODINFO_DATE_ID 4

struct snmp_vars sysctls[] = {
	{1,  ASN_INTEGER,   "protocol"},
	{2,  ASN_INTEGER,   "hashsize"},
	{3,  ASN_INTEGER,   "maxflows"},
	{4,  ASN_INTEGER,   "active_timeout"},
	{5,  ASN_INTEGER,   "inactive_timeout"},
	{6,  ASN_INTEGER,   "sndbuf"},
	{7,  ASN_OCTET_STR, "destination"},
	{8,  ASN_OCTET_STR, "aggregation"},
	{9,  ASN_OCTET_STR, "sampler"},
	{10, ASN_INTEGER,   "natevents"},
	{11, ASN_INTEGER,   "promisc"},
	{12, ASN_OCTET_STR, "snmp-rules"},
	{13, ASN_INTEGER,   "scan-min"},
	{ 0 }
};

struct snmp_vars totals[] = {
	{1,  ASN_COUNTER64, "inBitRate"},
	{2,  ASN_GAUGE,     "inPacketRate"},
	{3,  ASN_COUNTER64, "inFlows"},
	{4,  ASN_COUNTER64, "inPackets"},
	{5,  ASN_COUNTER64, "inBytes"},
	{6,  ASN_GAUGE,     "hashMetric"},
	{7,  ASN_GAUGE,     "hashMemory"},
	{8,  ASN_GAUGE,     "hashFlows"},
	{9,  ASN_GAUGE,     "hashPackets"},
	{10, ASN_COUNTER64, "hashBytes"},
	{11, ASN_COUNTER64, "dropPackets"},
	{12, ASN_COUNTER64, "dropBytes"},
	{13, ASN_GAUGE,     "outByteRate"},
	{14, ASN_COUNTER64, "outFlows"},
	{15, ASN_COUNTER64, "outPackets"},
	{16, ASN_COUNTER64, "outBytes"},
	{17, ASN_COUNTER64, "lostFlows"},
	{18, ASN_COUNTER64, "lostPackets"},
	{19, ASN_COUNTER64, "lostBytes"},
	{20, ASN_COUNTER,   "errTotal"},
	{21, ASN_COUNTER,   "sndbufPeak"},
	{ 0 }
};
#define TOTALS_METRIC_ID 6

static netsnmp_table_data_set *cpu_data_set;
static netsnmp_cache *stat_cache = NULL;

struct snmp_vars cputable[] = {
	{1,  ASN_INTEGER,   "cpuIndex"},
	{2,  ASN_GAUGE,     "cpuInPacketRate"},
	{3,  ASN_COUNTER64, "cpuInFlows"},
	{4,  ASN_COUNTER64, "cpuInPackets"},
	{5,  ASN_COUNTER64, "cpuInBytes"},
	{6,  ASN_GAUGE,     "cpuHashMetric"},
	{7,  ASN_COUNTER64, "cpuDropPackets"},
	{8,  ASN_COUNTER64, "cpuDropBytes"},
	{9,  ASN_COUNTER,   "cpuErrTrunc"},
	{10, ASN_COUNTER,   "cpuErrFrag"},
	{11, ASN_COUNTER,   "cpuErrAlloc"},
	{12, ASN_COUNTER,   "cpuErrMaxflows"},
	{ 0 }
};

static netsnmp_table_data_set *sock_data_set;
struct snmp_vars socktable[] = {
	{1,  ASN_INTEGER,   "sockIndex"},
	{2,  ASN_OCTET_STR, "sockDestination"},
	{3,  ASN_INTEGER,   "sockActive"},
	{4,  ASN_COUNTER,   "sockErrConnect"},
	{5,  ASN_COUNTER,   "sockErrFull"},
	{6,  ASN_COUNTER,   "sockErrCberr"},
	{7,  ASN_COUNTER,   "sockErrOther"},
	{8,  ASN_GAUGE,     "sockSndbuf"},
	{9,  ASN_GAUGE,     "sockSndbufFill"},
	{10, ASN_GAUGE,     "sockSndbufPeak"},
	{ 0 }
};

static time_t totals_ts; /* when statistics last read from kernel */

static int var_max(struct snmp_vars *head)
{
	struct snmp_vars *sys;
	int max = 0;

	for (sys = head; sys->obj; sys++)
		if (max < sys->obj)
			max = sys->obj;
	return max;
}

static struct snmp_vars *find_varinfo(struct snmp_vars *head, const int obj)
{
	struct snmp_vars *sys;

	for (sys = head; sys->obj; sys++) {
		if (sys->obj == obj)
			return sys;
	}
	return NULL;
}

static struct snmp_vars *find_varinfo_str(struct snmp_vars *head, const char *name)
{
	struct snmp_vars *sys;

	for (sys = head; sys->obj; sys++) {
		if (!strcmp(sys->name, name))
			return sys;
	}
	return NULL;
}

static void modinfo_fname(char *name, char *fname, size_t flen)
{
	snprintf(fname, flen, "/sys/module/" MODINFO_NAME "/%s", name);
}

static void sysctl_fname(char *name, char *fname, size_t flen)
{
	snprintf(fname, flen, "/proc/sys/net/netflow/%s", name);
}

static int sysctl_access_ok(char *name)
{
	char fname[64];

	sysctl_fname(name, fname, sizeof(fname));
	if (access(fname, W_OK) < 0)
		return 0;
	return 1;
}

static char *file_read_string(char *name, char *buf, size_t size)
{
	int fd = open(name, O_RDONLY);
	if (fd < 0)
		return NULL;
	int n = read(fd, buf, size - 1);
	if (n < 0) {
		close(fd);
		return NULL;
	}
	buf[n] = '\0';
	close(fd);
	return buf;
}

static char *modinfo_read_string(char *name, char *buf, size_t size)
{
	char fname[64];

	modinfo_fname(name, fname, sizeof(fname));
	return file_read_string(fname, buf, size);
}

static char *sysctl_read_string(char *name, char *buf, size_t size)
{
	char fname[64];

	sysctl_fname(name, fname, sizeof(fname));
	return file_read_string(fname, buf, size);
}

static int sysctl_write_string(char *name, char *buf, size_t size)
{
	char fname[64];
	int fd;
	int n;

	sysctl_fname(name, fname, sizeof(fname));
	fd = open(fname, O_RDWR, 0644);
	if (fd < 0)
		return fd;
	n = write(fd, buf, size);
	close(fd);
	return n;
}

static int sysctl_read(netsnmp_request_info *request, int obj)
{
	struct snmp_vars *sys = find_varinfo(sysctls, obj);
	char buf[225];
	char *p;
	long value;

	if (!sys)
		goto nosuchobject;

	p = sysctl_read_string(sys->name, buf, sizeof(buf));
	if (!p)
		goto nosuchobject;

	switch (sys->type) {
	case ASN_INTEGER:
		value = atoi(p);
		snmp_set_var_typed_value(request->requestvb,
		    sys->type,
		    (u_char *)&value, sizeof(value));
		return SNMP_ERR_NOERROR;
	case ASN_OCTET_STR:
		snmp_set_var_typed_value(request->requestvb,
		    sys->type,
		    (u_char *)p, strcspn(p, "\n"));
		return SNMP_ERR_NOERROR;
	}
nosuchobject:
	netsnmp_request_set_error(request, SNMP_NOSUCHOBJECT);
	return SNMP_ERR_NOERROR;
}

static int sysctl_write(netsnmp_request_info *request, int obj)
{
	struct snmp_vars *sys = find_varinfo(sysctls, obj);
	char buf[225];
	int len;

	if (!sys) {
		netsnmp_request_set_error(request, SNMP_NOSUCHOBJECT);
		return SNMP_ERR_NOERROR;
	}
	switch (sys->type) {
	case ASN_INTEGER:
		snprintf(buf, sizeof(buf), "%ld\n", *(request->requestvb->val.integer));
		break;
	case ASN_UNSIGNED:
		snprintf(buf, sizeof(buf), "%lu\n", *(request->requestvb->val.integer));
		break;
	case ASN_OCTET_STR:
		snprintf(buf, sizeof(buf), "%s\n", request->requestvb->val.string);
		break;
	default:
		netsnmp_request_set_error(request, SNMP_ERR_WRONGTYPE);
		return SNMP_ERR_NOERROR;
	}
	len = strlen(buf);
	if (sysctl_write_string(sys->name, buf, len) < len)
		netsnmp_request_set_error(request, SNMP_ERR_BADVALUE);
	return SNMP_ERR_NOERROR;
}

static int iptNetflowModule_handler(
    netsnmp_mib_handler          *handler,
    netsnmp_handler_registration *reginfo,
    netsnmp_agent_request_info   *reqinfo,
    netsnmp_request_info         *request)
{
	struct snmp_vars *sys;
	oid obj;
	char buf[225];
	char *p = NULL;
	long value;

	obj = request->requestvb->name[request->requestvb->name_length - 2];
	sys = find_varinfo(modinfos, obj);
	if (!sys) {
		netsnmp_request_set_error(request, SNMP_ERR_NOSUCHNAME);
		return SNMP_ERR_NOERROR;
	}
	if (reqinfo->mode != MODE_GET) {
		netsnmp_request_set_error(request, SNMP_ERR_READONLY);
		return SNMP_ERR_NOERROR;
	}
	switch (obj) {
	case MODINFO_NAME_ID:
		p = MODINFO_NAME;
		break;
	case MODINFO_DATE_ID: {
		size_t len;
		struct stat st;

		modinfo_fname(".", buf, sizeof(buf));
		if (stat(buf, &st) < 0)
			break;
		p = (char *)date_n_time(&st.st_mtime, &len);
		snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, p, len);
		return SNMP_ERR_NOERROR;
	}
	default:
		p = modinfo_read_string(sys->name, buf, sizeof(buf));
	}
	if (!p) {
		netsnmp_request_set_error(request, SNMP_ERR_NOSUCHNAME);
		return SNMP_ERR_NOERROR;
	}

	switch (sys->type) {
	case ASN_INTEGER:
		value = atoi(p);
		snmp_set_var_typed_value(request->requestvb,
		    sys->type,
		    (u_char *)&value, sizeof(value));
		break;
	case ASN_OCTET_STR:
		snmp_set_var_typed_value(request->requestvb,
		    sys->type,
		    (u_char *)p, strcspn(p, "\n"));
		break;
	default:
		netsnmp_request_set_error(request, SNMP_ERR_WRONGTYPE);

	}
	return SNMP_ERR_NOERROR;
}

static int iptNetflowSysctl_handler(
    netsnmp_mib_handler          *handler,
    netsnmp_handler_registration *reginfo,
    netsnmp_agent_request_info   *reqinfo,
    netsnmp_request_info         *request)
{
	struct snmp_vars *sys;
	oid obj;

	obj = request->requestvb->name[request->requestvb->name_length - 2];
	switch (reqinfo->mode) {
	case MODE_GET:
		return sysctl_read(request, obj);
	case MODE_SET_RESERVE1:
		sys = find_varinfo(sysctls, obj);
		if (!sys || !sysctl_access_ok(sys->name))
			netsnmp_request_set_error(request, SNMP_ERR_NOSUCHNAME);
		if (sys && request->requestvb->type != sys->type)
			netsnmp_request_set_error(request, SNMP_ERR_WRONGTYPE);
		break;
	case MODE_SET_RESERVE2:
	case MODE_SET_FREE:
	case MODE_SET_UNDO:
	case MODE_SET_COMMIT:
		return SNMP_ERR_NOERROR;
	case MODE_SET_ACTION:
		return sysctl_write(request, obj);
	default:
		return SNMP_ERR_GENERR;

	}
	return SNMP_ERR_NOERROR;
}

#define TOTAL_INTERVAL 1

static void clear_data_set(netsnmp_table_data_set *data_set)
{
	netsnmp_table_row *row, *nextrow;

	for (row = netsnmp_table_data_set_get_first_row(data_set); row; row = nextrow) {
		nextrow = netsnmp_table_data_set_get_next_row(data_set, row);
		netsnmp_table_dataset_remove_and_delete_row(data_set, row);
	}
}

static void parse_table_row(
    int			    cpu,
    char		    *p,
    struct snmp_vars	    *sys,
    netsnmp_table_data_set  *data_set)
{
	netsnmp_table_row *row;

	row = netsnmp_create_table_data_row();
	netsnmp_table_row_add_index(row, ASN_INTEGER, (u_char *)&cpu, sizeof(cpu));

	if (sys == cputable) {
		/* add cpuIndex as column too to break SMIv2 */
		netsnmp_set_row_column(row, 1, sys->type, (char *)&cpu, sizeof(cpu));
	}
	for (++sys; p && sys->obj; sys++) {
		char		 *val;
		long long	 val64;
		unsigned int	 uval32;
		int		 val32;
		struct counter64 c64;

		p += strspn(p, " \t");
		val = p;
		if ((p = strpbrk(p, " \t")))
			*p++ = '\0';
		if (index(val, '.')) {
			double d = strtod(val, NULL);

			val64 = (long long)(d * 100);
		} else
			val64 = strtoll(val, NULL, 10);

		switch (sys->type) {
		case ASN_OCTET_STR:
			netsnmp_set_row_column(row, sys->obj,
			    sys->type, (char *)val, strlen(val));
			break;
		case ASN_INTEGER:
		case ASN_GAUGE:
			val32 = (int)val64;
			netsnmp_set_row_column(row, sys->obj,
			    sys->type, (char *)&val32, sizeof(val32));
			break;
		case ASN_COUNTER:
			uval32 = (unsigned int)val64;
			netsnmp_set_row_column(row, sys->obj,
			    sys->type, (char *)&uval32, sizeof(uval32));
			break;
		case ASN_COUNTER64:
			c64.low = (uint32_t)val64;
			c64.high = val64 >> 32;
			netsnmp_set_row_column(row, sys->obj,
			    sys->type, (char *)&c64, sizeof(c64));
			break;
		default:
			netsnmp_table_dataset_delete_row(row);
			continue;
		}

	}
	netsnmp_table_data_add_row(data_set->table, row);
}

static void grab_ipt_netflow_snmp(time_t now)
{
	static char buf[4096];
	int fd;
	int n;
	char *p = buf;

	if ((now - totals_ts) < (TOTAL_INTERVAL + 1))
		return;

	if ((fd = open("/proc/net/stat/ipt_netflow_snmp", O_RDONLY)) < 0)
		return;

	n = read(fd, buf, sizeof(buf) - 1);
	close(fd);
	if (n <= 0)
		return;
	buf[n] = '\0';

	DEBUGMSGTL(("netflow", "%s\n", buf));
	clear_data_set(cpu_data_set);
	clear_data_set(sock_data_set);
	while (*p) {
		struct snmp_vars *sys;
		char *name = p;
		char *val;

		if (!(p = strpbrk(p, " \t")))
			break;
		*p++ = '\0';
		val = p + strspn(p, " \t");
		p = index(p, '\n');
		*p++ = '\0';

		if (!strncmp(name, "cpu", 3)) {
			parse_table_row(atoi(name + 3), val, cputable, cpu_data_set);
			continue;
		} else if (!strncmp(name, "sock", 4)) {
			parse_table_row(atoi(name + 4), val, socktable, sock_data_set);
			continue;
		}
		if (!(sys = find_varinfo_str(totals, name)))
		    continue;
		if (index(val, '.')) {
			double d = strtod(val, NULL);
			sys->val64 = (long long)(d * 100);
		} else
			sys->val64 = strtoll(val, NULL, 10);
		sys->ts = now;
	}
	totals_ts = now;
}

static int iptNetflowTotals_handler(
    netsnmp_mib_handler          *handler,
    netsnmp_handler_registration *reginfo,
    netsnmp_agent_request_info   *reqinfo,
    netsnmp_request_info         *request)
{
	struct snmp_vars *sys;
	time_t now = time(NULL);
	oid obj;
	unsigned int	 uval32;
	int		 val32;
	struct counter64 c64;

	grab_ipt_netflow_snmp(now);

	obj = request->requestvb->name[request->requestvb->name_length - 2];
	sys = find_varinfo(totals, obj);
	if (!sys || ((now - sys->ts) > (TOTAL_INTERVAL * 2 + 3))) {
		netsnmp_request_set_error(request, SNMP_ERR_NOSUCHNAME);
		return SNMP_ERR_NOERROR;
	}
	if (reqinfo->mode != MODE_GET) {
		netsnmp_request_set_error(request, SNMP_ERR_READONLY);
		return SNMP_ERR_NOERROR;
	}
	switch (sys->type) {
	case ASN_GAUGE:
		val32 = (int)sys->val64;
		snmp_set_var_typed_value(request->requestvb,
		    sys->type, (u_char *)&val32, sizeof(val32));
		break;
	case ASN_COUNTER:
		uval32 = (unsigned int)sys->val64;
		snmp_set_var_typed_value(request->requestvb,
		    sys->type, (u_char *)&uval32, sizeof(uval32));
		break;
	case ASN_COUNTER64:
		c64.low = (uint32_t)sys->val64;
		c64.high = sys->val64 >> 32;
		snmp_set_var_typed_value(request->requestvb,
		    ASN_COUNTER64, (u_char *)&c64, sizeof(c64));
		break;
	default:
		return SNMP_ERR_GENERR;
	}
	return SNMP_ERR_NOERROR;
}

static int stat_cache_load(netsnmp_cache *cache, void *x)
{
	grab_ipt_netflow_snmp(time(NULL));
	return 0;
}

static void dummy_cache_free(netsnmp_cache *cache, void *x)
{
	/* free_cache callback is not always checked for NULL
	 * pointer. */
}

void init_netflow(void)
{
	netsnmp_handler_registration *reg;
	struct snmp_vars *sys;

	/* snmpd -f -L -Dnetflow,dlmod */
	DEBUGMSGTL(("netflow", "init_netflow\n"));

	netsnmp_register_scalar_group(
	    netsnmp_create_handler_registration(
		    "iptNetflowModule",
		    iptNetflowModule_handler,
		    iptNetflowModule_oid,
		    OID_LENGTH(iptNetflowModule_oid),
		    HANDLER_CAN_RONLY),
	    1, var_max(modinfos));

	netsnmp_register_scalar_group(
	    netsnmp_create_handler_registration(
		    "iptNetflowSysctl",
		    iptNetflowSysctl_handler,
		    iptNetflowSysctl_oid,
		    OID_LENGTH(iptNetflowSysctl_oid),
		    HANDLER_CAN_RWRITE),
	    1, var_max(sysctls));

	netsnmp_register_scalar_group(
	    netsnmp_create_handler_registration(
		    "iptNetflowTotals",
		    iptNetflowTotals_handler,
		    iptNetflowTotals_oid,
		    OID_LENGTH(iptNetflowTotals_oid),
		    HANDLER_CAN_RONLY),
	    1, var_max(totals));

	/* Register first table. */
	reg = netsnmp_create_handler_registration(
	    "iptNetflowCpuTable", /* no handler */ NULL,
	    iptNetflowCpuTable_oid, OID_LENGTH(iptNetflowCpuTable_oid),
	    HANDLER_CAN_RONLY);

	/* set up columns */
	cpu_data_set = netsnmp_create_table_data_set("iptNetflowCpuDataSet");
	netsnmp_table_set_add_indexes(cpu_data_set, ASN_INTEGER, 0);
	/* I include cpuIndex into columns, which is not SMIv2'ish */
	for (sys = cputable; sys->obj; sys++)
		netsnmp_table_set_add_default_row(cpu_data_set, sys->obj, sys->type, 0, NULL, 0);
	netsnmp_register_table_data_set(reg, cpu_data_set, NULL);

	/* cache handler will load actual data, and it needs to be
	 * injected in front of dataset handler to be called first */
	stat_cache = netsnmp_cache_create(
	    /* no timeout */ -1,
	    stat_cache_load, dummy_cache_free,
	    iptNetflowCpuTable_oid, OID_LENGTH(iptNetflowCpuTable_oid));
	netsnmp_inject_handler(reg, netsnmp_cache_handler_get(stat_cache));

	/* Register second table. */
	reg = netsnmp_create_handler_registration(
	    "iptNetflowSockTable", /* no handler */ NULL,
	    iptNetflowSockTable_oid, OID_LENGTH(iptNetflowSockTable_oid),
	    HANDLER_CAN_RONLY);

	/* set up columns */
	sock_data_set = netsnmp_create_table_data_set("iptNetflowSockDataSet");
	/* I don't include sockIndex into columns, which is more SMIv2'ish */
	netsnmp_table_set_add_indexes(sock_data_set, ASN_INTEGER, 0);
	for (sys = &socktable[1]; sys->obj; sys++)
		netsnmp_table_set_add_default_row(sock_data_set, sys->obj, sys->type, 0, NULL, 0);
	netsnmp_register_table_data_set(reg, sock_data_set, NULL);

	/* as before, cache handler will load actual data, and it needs
	 * to be injected in front of dataset handler to be called first */
	stat_cache = netsnmp_cache_create(
	    /* no timeout */ -1,
	    stat_cache_load, dummy_cache_free,
	    iptNetflowSockTable_oid, OID_LENGTH(iptNetflowSockTable_oid));
	netsnmp_inject_handler(reg, netsnmp_cache_handler_get(stat_cache));
}

void deinit_netflow(void)
{
	DEBUGMSGTL(("netflow", "deinit_netflow\n"));
}

