/*\
 *	DISTRIBUTION: HNMS v2.0
 *	FILE: hnmsd/icmp.c
 *
 *	ICMP for HNMS I/O module.
 *
 *	Jude George
 *	NAS Facility, NASA Ames Research Center
 *
 *	Copyright (c) 1994 Jude George
 *
 *	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 1, 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; if not, write to the Free Software
 *	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
\*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in_systm.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <netdb.h>
#include <sys/time.h>

#include "stdhnms.h"

#define ICMP_BUFSIZ		4096

static int			icmp_sock = 0;
static struct sockaddr_in	icmp_remote_addr;
static unsigned char		icmp_buf[ICMP_BUFSIZ];
static unsigned short		icmp_id;
static short			seq = 0;
static int			echo_interval;
static unsigned int		echoes_sent[MAX_OBJECTS];
static unsigned int		echoes_received[MAX_OBJECTS];
static unsigned int		ipaddr_cache[MAX_OBJECTS];

/*\
 *  Initialize ICMP routines.
\*/
int ICMP_init()
{
    struct protoent		*protocol;
    int				parm;
    static int			first_init = 0;

    if (!first_init) {
	first_init = 1;
	bzero(echoes_sent, MAX_OBJECTS * sizeof(int));
	bzero(echoes_received, MAX_OBJECTS * sizeof(int));
    }

    if (icmp_sock > 0)
	close(icmp_sock);
    protocol = getprotobyname("icmp");
    if (!protocol) {
	hnms_errno = HNMS_err_sock_open;
	return -1;
    }
    icmp_sock = socket(AF_INET, SOCK_RAW, protocol->p_proto);
    if (icmp_sock <= 0) {
	hnms_errno = HNMS_err_sock_open;
	return -1;
    }
    parm = 1;
    if (ioctl(icmp_sock, FIONBIO, &parm) < 0) {
	close(icmp_sock);
	hnms_errno = HNMS_err_sock_nbio;
	return -1;
    }
    icmp_id = getpid() & 0xffff;
    return 0;
}

/*\
 *  Send out an echo-request to the specified Ipaddr, if it has been
 *  more than echo_interval seconds since I last sent an echo-request.
 *  We use a data portion consisting of a timestamp, just like ping.
\*/
static int ICMP_send_echo(hnms_id)
    const int			hnms_id;
{
    int				class;
    unsigned int		ip_address;
    unsigned short		*sp;
    int				sum;
    int				n;
    int				len;
    unsigned int		echo_ts, current_time;
    static struct timeval	tv;
    static struct timezone	tz;
    static int			randomized = 0;
    static unsigned int		offset = 0;

    class = 0;
    OBJ_get(hnms_id, oid_hnmsObjClass, &class, 0, 0, 0, 0);
    if (class != OBJ_ipaddr)
	return -1;

    ip_address = 0;
    OBJ_get(hnms_id, oid_hnmsObjIpaddr, &ip_address, 0, 0, 0, 0);
    if (!ip_address)
	return -1;

    ipaddr_cache[hnms_id] = ip_address;

    current_time = get_int_time();
    if (!randomized) {
	randomized = 1;
	srandom(current_time);
    }
    echo_ts = 0;
    OBJ_get(hnms_id, oid_hnmsIpaddrLastEchoOut, &echo_ts, 0, 0, 0, 0);
    if (echo_ts == 0) {
	echo_ts =
	    (unsigned int)(current_time - (++offset % (echo_interval / 5)));
	OBJ_set(hnms_id, oid_hnmsIpaddrLastEchoOut, &echo_ts, 0,
		MIB_integer, 0, 0);
    }

    if ((unsigned int)(current_time - echo_ts) >= echo_interval) {
	echo_ts = current_time - (random() % (echo_interval / 5));
	OBJ_set(hnms_id, oid_hnmsIpaddrLastEchoOut, &echo_ts, 0,
		MIB_integer, 0, &current_time);
	bzero(icmp_buf, ICMP_BUFSIZ);
	sp = (unsigned short *)icmp_buf;
	
	/*
	 * ICMP header.
	 * Type = 8 (echo request), code = 0, cksum = 0, ident = my pid,
	 * seq = integer val.
	 */
	icmp_buf[0] = 8;
	icmp_buf[1] = 0;
	sp[2] = htons(icmp_id);
	sp[3] = htons(seq++);
	
	/*
	 * Data portion.
	 */
	gettimeofday(&tv, &tz);
	bcopy(&tv, &(icmp_buf[8]), 8);
	
	len = 64;
	
	/*
	 * Muuss's checksum.
	 */
	sum = 0;
	for (n = 0; n < 32; n++)
	    sum += sp[n];
	sum = (sum >> 16) + (sum & 0xffff);
	sum += (sum >> 16);
	sp[1] = ~sum;

	icmp_remote_addr.sin_addr.s_addr = htonl(ip_address);
	icmp_remote_addr.sin_port = 0;
	if (sendto(icmp_sock, icmp_buf, len, 0, &icmp_remote_addr,
		   sizeof(struct sockaddr_in)) < 0) {
	    hnms_errno = HNMS_err_sendto;
	    HNMS_debug(DEBUG_ERRORS, "error: ICMP_send_echo\n");
	}
	echoes_sent[hnms_id]++;
	HNMS_debug(DEBUG_ICMP, "ICMP: echo to %x\n", ip_address);
    }
    return 0;
}

/*\
 *  Poll Ipaddrs to get their reachability status, via ICMP echo.
\*/
int ICMP_status_poll()
{
    if (icmp_sock < 0)
	return 0;

    echo_interval = PARAM_get_int(oid_hnmsModuleIoReachInterval);

    OBJ_iterate(ICMP_send_echo, NULL);
    return 0;
}

/*\
 *  Retrieve an inbound ICMP packet.  Set the status of the source Ipaddr
 *  to REACHABLE, and save the round-trip time as a variable.  The RTT
 *  will always be long by some amount of time, which is equal to the
 *  event lag of the module (usually about 1.5s).
 *
 *  NOTE: At least on SGIs, the receive buffer for our SOCK_RAW is flaky,
 *  so we don't trust it... hence we also "listen" for our returned pings
 *  in LL2_listen() via tcpdump.  This function, as well as ICMP_ready()
 *  and ICMP_listen(), may be effectively removed if we can rely on tcpdump.
\*/
static void ICMP_recv()
{
    int				rval;
    unsigned int		ip_address;
    int				remote_len;
    unsigned char		type;
    unsigned short		ident;
    char			*uname;
    int				status;
    static struct timeval	tv_out, tv_in;
    static struct timezone	tz;
    unsigned int		rtt;
    unsigned int		time;
    int				hnms_id;

    remote_len = sizeof(struct sockaddr_in);

    /*
     * recvfrom() gives us the entire IP packet.
     */
    bzero(icmp_buf, ICMP_BUFSIZ);
    if ((rval = recvfrom(icmp_sock, icmp_buf, ICMP_BUFSIZ, 0,
			 &icmp_remote_addr, &remote_len)) < 0)
	HNMS_debug(DEBUG_ERRORS, "error: ICMP_recv\n");
    if (rval == 0)
	return;

    type = icmp_buf[20];
    ident = ntohs(*(short *)(&(icmp_buf[24])));

    for (hnms_id = 1; hnms_id < MAX_OBJECTS; hnms_id++)
	if (ipaddr_cache[hnms_id] == ntohl(icmp_remote_addr.sin_addr.s_addr))
	    break;
    if (hnms_id == MAX_OBJECTS)
	return;

    if (type != 0 || ident != icmp_id)	
	return;

    if (!OBJ_exists(hnms_id))
	return;

    /*
     * Calculate round-trip time and store it as a number expressed
     * in milliseconds (not microseconds).  All the RTT values will
     * be long by some amount which is equal to the event lag of this
     * module (usually about 1.5s).
     */
    bcopy(&(icmp_buf[28]), (char *)&tv_out, sizeof(tv_out));
	gettimeofday(&tv_in, &tz);
	tv_in.tv_sec -= tv_out.tv_sec;
	tv_in.tv_usec -= tv_out.tv_usec;
	if (tv_in.tv_usec < 0) {
	    tv_in.tv_usec += 1000000;
	    tv_in.tv_sec--;
	}
    /*
     * We approximate.
     */
    rtt = (tv_in.tv_sec << 10) + (tv_in.tv_usec >> 10);
    status = STATUS_REACHABLE;
    time = get_int_time();
    OBJ_set(hnms_id, oid_hnmsIpaddrLastEchoIn, &time,
	    0, MIB_integer, 0, &time);
    OBJ_set(hnms_id, oid_hnmsIpaddrRTT, &rtt, 0, MIB_integer, 0, &time);
    OBJ_set(hnms_id, oid_hnmsObjReachStatus, &status,
	    0, MIB_integer, 0, &time);
    echoes_received[hnms_id]++;
    HNMS_debug(DEBUG_ICMP, "ICMP: received echo from %x\n",
	       ipaddr_cache[hnms_id]);
}

/*\
 *  Check if I have ICMP packets waiting at the icmp socket.  Return a
 *  positive integer if I do, 0 otherwise.
\*/
static int ICMP_ready()
{
    struct timeval	select_time;
    fd_set		icmp_read_ready;
    int			ready_descs;

    if (icmp_sock < 0)
	return 0;
    select_time.tv_sec = 0;
    select_time.tv_usec = 0;
    FD_ZERO(&icmp_read_ready);
    FD_SET(icmp_sock, &icmp_read_ready);

    if ((ready_descs = select(getdtablesize(), &icmp_read_ready, 0,
			      0, &select_time)) < 0) {
	HNMS_debug(DEBUG_ERRORS, "ICMP select()\n");
	return 0;
    }

    return ready_descs;
}

/*\
 *  Listen for incoming ICMP packets.
\*/
int ICMP_listen()
{
    while(ICMP_ready())
	ICMP_recv();
    return 0;
}
