/*-
 * Copyright (c) 1985, 1993
 *	The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * Clockdiff computes the difference between the time of the machine on which it is
 * called and the time of the machines given as argument.  The time differences measured
 * by clockdiff are obtained using a sequence of ICMP TSTAMP messages which are returned
 * to the sender by the IP module in the remote machine.
 *
 * In order to compare clocks of machines in different time zones, the time is
 * transmitted (as a 32-bit value) in milliseconds since midnight UT.  If a hosts uses a
 * different time format, it should set the high order bit of the 32-bit quantity it
 * transmits.
 *
 * However, VMS apparently transmits the time in milliseconds since midnight local time
 * (rather than GMT) without setting the high order bit.  Furthermore, it does not
 * understand daylight-saving time.  This makes clockdiff behaving inconsistently with
 * hosts running VMS.
 *
 * In order to reduce the sensitivity to the variance of message transmission time,
 * clockdiff sends a sequence of messages.  Yet, measures between two `distant' hosts can
 * be affected by a small error.  The error can, however, be reduced by increasing the
 * number of messages sent in each measurement.
 */

#define _GNU_SOURCE

#define TSPTYPES

#include <arpa/inet.h>
#include <errno.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <linux/types.h>
#include <math.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/timex.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#ifdef HAVE_LIBCAP
# include <sys/capability.h>
#endif

#include "iputils_common.h"

enum {
	RANGE = 1,		/* best expected round-trip time, ms */
	MSGS = 50,
	TRIALS = 10,

	GOOD = 0,
	UNREACHABLE = 2,
	NONSTDTIME = 3,
	BREAK = 4,
	CONTINUE = 5,
	HOSTDOWN = 0x7fffffff,

	BIASP = 43199999,
	BIASN = -43200000,
	MODULO =  86400000,
	PROCESSING_TIME	= 0,	/* ms. to reduce error in measurement */

	PACKET_IN = 1024
};

enum {
	time_format_ctime,
	time_format_iso
};

struct run_state {
	int interactive;
	uint16_t id;
	int sock_raw;
	struct sockaddr_in server;
	int ip_opt_len;
	int measure_delta;
	int measure_delta1;
	unsigned short seqno;
	unsigned short seqno0;
	unsigned short acked;
	long rtt;
	long min_rtt;
	long rtt_sigma;
	char *hisname;
	int time_format;
};

struct measure_vars {
	struct timespec ts1;
	struct timespec tout;
	int count;
	int cc;
	unsigned char packet[PACKET_IN];
	socklen_t length;
	struct icmphdr *icp;
	struct iphdr *ip;
	int msgcount;
	long min1;
	long min2;
};

/*
 * All includes, definitions, struct declarations, and global variables are above.  After
 * this comment all you can find is functions.
 */

/*
 * addcarry() - checksum routine for Internet Protocol family headers.
 *
 * This routine is very heavily used in the network code and should be modified for each
 * CPU to be as fast as possible.
 *
 * This implementation is TAHOE version.
 */
static inline int addcarry(int sum)
{
	if (sum & 0xffff0000) {
		sum &= 0xffff;
		sum++;
	}
	return sum;
}

static int in_cksum(unsigned short *addr, int len)
{
	union word {
		char c[2];
		unsigned short s;
	} u;
	int sum = 0;

	while (len > 0) {
		/* add by words */
		while ((len -= 2) >= 0) {
			if ((unsigned long)addr & 0x1) {
				/* word is not aligned */
				u.c[0] = *(char *)addr;
				u.c[1] = *((char *)addr + 1);
				sum += u.s;
				addr++;
			} else
				sum += *addr++;
			sum = addcarry(sum);
		}
		if (len == -1)
			/* odd number of bytes */
			u.c[0] = *(unsigned char *)addr;
	}
	if (len == -1) {
		/*
		 * The last mbuf has odd # of bytes.  Follow the standard (the odd byte
		 * is shifted left by 8 bits)
		 */
		u.c[1] = 0;
		sum += u.s;
		sum = addcarry(sum);
	}
	return (~sum & 0xffff);
}

static int measure_inner_loop(struct run_state *ctl, struct measure_vars *mv)
{
	long delta1;
	long delta2;
	long diff;
	long histime = 0;
	long histime1 = 0;
	long recvtime;
	long sendtime;
	struct pollfd p = { .fd = ctl->sock_raw, .events = POLLIN | POLLHUP };

	{
		long tmo = MAX(ctl->rtt + ctl->rtt_sigma, 1);

		mv->tout.tv_sec = tmo / 1000;
		mv->tout.tv_nsec = (tmo - (tmo / 1000) * 1000) * 1000000;
	}

	if ((mv->count = ppoll(&p, 1, &mv->tout, NULL)) <= 0)
		return BREAK;

	clock_gettime(CLOCK_REALTIME, &mv->ts1);
	mv->cc = recvfrom(ctl->sock_raw, (char *)mv->packet, PACKET_IN, 0, NULL, &mv->length);

	if (mv->cc < 0)
		return (-1);

	mv->icp = (struct icmphdr *)(mv->packet + (mv->ip->ihl << 2));

	if (((ctl->ip_opt_len && mv->icp->type == ICMP_ECHOREPLY
	      && mv->packet[20] == IPOPT_TIMESTAMP)
	     || mv->icp->type == ICMP_TIMESTAMPREPLY)
	    && mv->icp->un.echo.id == ctl->id && mv->icp->un.echo.sequence >= ctl->seqno0
	    && mv->icp->un.echo.sequence <= ctl->seqno) {
		int i;
		uint8_t *opt = mv->packet + 20;

		if (ctl->acked < mv->icp->un.echo.sequence)
			ctl->acked = mv->icp->un.echo.sequence;
		if (ctl->ip_opt_len) {
			if ((opt[3] & 0xF) != IPOPT_TS_PRESPEC) {
				fprintf(stderr, _("Wrong timestamp %d\n"), opt[3] & 0xF);
				return NONSTDTIME;
			}
			if (opt[3] >> 4) {
				if ((opt[3] >> 4) != 1 || ctl->ip_opt_len != 4 + 3 * 8)
					fprintf(stderr, _("Overflow %d hops\n"), opt[3] >> 4);
			}
			sendtime = recvtime = histime = histime1 = 0;
			for (i = 0; i < (opt[2] - 5) / 8; i++) {
				uint32_t *timep = (uint32_t *) (opt + 4 + i * 8 + 4);
				uint32_t t = ntohl(*timep);

				if (t & 0x80000000)
					return NONSTDTIME;

				if (i == 0)
					sendtime = t;
				if (i == 1)
					histime = histime1 = t;
				if (i == 2) {
					if (ctl->ip_opt_len == 4 + 4 * 8)
						histime1 = t;
					else
						recvtime = t;
				}
				if (i == 3)
					recvtime = t;
			}

			if (!(sendtime & histime & histime1 & recvtime)) {
				fprintf(stderr, _("wrong timestamps\n"));
				return -1;
			}
		} else {
			recvtime = (mv->ts1.tv_sec % (24 * 60 * 60)) * 1000 +
					mv->ts1.tv_nsec / 1000000;
			sendtime = ntohl(*(uint32_t *) (mv->icp + 1));
		}
		diff = recvtime - sendtime;
		/* diff can be less than 0 around midnight */
		if (diff < 0)
			return CONTINUE;
		ctl->rtt = (ctl->rtt * 3 + diff) / 4;
		ctl->rtt_sigma = (ctl->rtt_sigma * 3 + labs(diff - ctl->rtt)) / 4;
		mv->msgcount++;
		if (!ctl->ip_opt_len) {
			histime = ntohl(((uint32_t *) (mv->icp + 1))[1]);
			/*
			 * a hosts using a time format different from ms.  since midnight
			 * UT (as per RFC792) should set the high order bit of the 32-bit
			 * time value it transmits.
			 */
			if ((histime & 0x80000000) != 0)
				return NONSTDTIME;
		}
		if (ctl->interactive) {
			printf(".");
			fflush(stdout);
		}

		delta1 = histime - sendtime;
		/*
		 * Handles wrap-around to avoid that around midnight small time
		 * differences appear enormous.  However, the two machine's clocks must
		 * be within 12 hours from each other.
		 */
		if (delta1 < BIASN)
			delta1 += MODULO;
		else if (delta1 > BIASP)
			delta1 -= MODULO;

		if (ctl->ip_opt_len)
			delta2 = recvtime - histime1;
		else
			delta2 = recvtime - histime;
		if (delta2 < BIASN)
			delta2 += MODULO;
		else if (delta2 > BIASP)
			delta2 -= MODULO;

		if (delta1 < mv->min1)
			mv->min1 = delta1;
		if (delta2 < mv->min2)
			mv->min2 = delta2;
		if (delta1 + delta2 < ctl->min_rtt) {
			ctl->min_rtt = delta1 + delta2;
			ctl->measure_delta1 = (delta1 - delta2) / 2 + PROCESSING_TIME;
		}
		if (diff < RANGE) {
			mv->min1 = delta1;
			mv->min2 = delta2;
			return BREAK;
		}
	}
	return CONTINUE;
}

/*
 * Measures the differences between machines' clocks using ICMP timestamp messages.
 */
static int measure(struct run_state *ctl)
{
	struct measure_vars mv = {
		.min1 = 0x7fffffff,
		.min2 = 0x7fffffff
	};
	unsigned char opacket[64] = { 0 };
	struct icmphdr *oicp = (struct icmphdr *)opacket;
	struct pollfd p = { .fd = ctl->sock_raw, .events = POLLIN | POLLHUP };

	mv.ip = (struct iphdr *)mv.packet;
	ctl->min_rtt = 0x7fffffff;
	ctl->measure_delta = HOSTDOWN;
	ctl->measure_delta1 = HOSTDOWN;

	/* empties the icmp input queue */
 empty:
	if (ppoll(&p, 1, &mv.tout, NULL)) {
		mv.length = sizeof(struct sockaddr_in);
		mv.cc = recvfrom(ctl->sock_raw, (char *)mv.packet, PACKET_IN, 0,
			      NULL, &mv.length);
		if (mv.cc < 0)
			return -1;
		goto empty;
	}

	/*
	 * To measure the difference, select MSGS messages whose round-trip time is
	 * smaller than RANGE if ckrange is 1, otherwise simply select MSGS messages
	 * regardless of round-trip transmission time.  Choose the smallest transmission
	 * time in each of the two directions.  Use these two latter quantities to
	 * compute the delta between the two clocks.
	 */

	mv.length = sizeof(struct sockaddr_in);
	if (ctl->ip_opt_len)
		oicp->type = ICMP_ECHO;
	else
		oicp->type = ICMP_TIMESTAMP;
	oicp->code = 0;
	oicp->checksum = 0;
	oicp->un.echo.id = ctl->id;
	((uint32_t *) (oicp + 1))[0] = 0;
	((uint32_t *) (oicp + 1))[1] = 0;
	((uint32_t *) (oicp + 1))[2] = 0;

	ctl->acked = ctl->seqno = ctl->seqno0 = 0;

	for (mv.msgcount = 0; mv.msgcount < MSGS;) {
		char escape = 0;

		/*
		 * If no answer is received for TRIALS consecutive times, the machine is
		 * assumed to be down
		 */
		if (ctl->seqno - ctl->acked > TRIALS) {
			errno = EHOSTDOWN;
			return HOSTDOWN;
		}

		oicp->un.echo.sequence = ++ctl->seqno;
		oicp->checksum = 0;

		clock_gettime(CLOCK_REALTIME, &mv.ts1);
		*(uint32_t *) (oicp + 1) =
		    htonl((mv.ts1.tv_sec % (24 * 60 * 60)) * 1000 + mv.ts1.tv_nsec / 1000000);
		oicp->checksum = in_cksum((unsigned short *)oicp, sizeof(*oicp) + 12);

		mv.count = sendto(ctl->sock_raw, (char *)opacket, sizeof(*oicp) + 12, 0,
			       (struct sockaddr *)&ctl->server, sizeof(struct sockaddr_in));

		if (mv.count < 0) {
			errno = EHOSTUNREACH;
			return UNREACHABLE;
		}

		while (!escape) {
			int ret = measure_inner_loop(ctl, &mv);

			switch (ret) {
				case BREAK:
					escape = 1;
					break;
				case CONTINUE:
					continue;
				default:
					return ret;
			}
		}
	}
	ctl->measure_delta = (mv.min1 - mv.min2) / 2 + PROCESSING_TIME;
	return GOOD;
}

static void drop_rights(void)
{
#ifdef HAVE_LIBCAP
	cap_t caps = cap_init();

	if (cap_set_proc(caps))
		error(-1, errno, "cap_set_proc");
	cap_free(caps);
#endif
	if (setuid(getuid()))
		error(-1, errno, "setuid");
}

static void usage(int exit_status)
{
	drop_rights();
	fprintf(stderr, _(
		"\nUsage:\n"
		"  clockdiff [options] <destination>\n"
		"\nOptions:\n"
		"                without -o, use icmp timestamp only (see RFC0792, page 16)\n"
		"  -o            use IP timestamp and icmp echo\n"
		"  -o1           use three-term IP timestamp and icmp echo\n"
		"  -T, --time-format <ctime|iso>\n"
		"                  specify display time format, ctime is the default\n"
		"  -I            alias of --time-format=iso\n"
		"  -h, --help    display this help\n"
		"  -V, --version print version and exit\n"
		"  <destination> DNS name or IP address\n"
		"\nFor more details see clockdiff(8).\n"));
	exit(exit_status);
}

static void parse_opts(struct run_state *ctl, int argc, char **argv)
{
	static const struct option longopts[] = {
		{"time-format", required_argument, NULL, 'T'},
		{"version", no_argument, NULL, 'V'},
		{"help", no_argument, NULL, 'h'},
		{NULL, 0, NULL, 0}
	};
	int c;

	while ((c = getopt_long(argc, argv, "o1T:IVh", longopts, NULL)) != -1)
		switch (c) {
		case 'o':
			ctl->ip_opt_len = 4 + 4 * 8;
			break;
		case '1':
			ctl->ip_opt_len = 4 + 3 * 8;
			break;
		case 'T':
			if (!strcmp(optarg, "iso"))
				ctl->time_format = time_format_iso;
			else if (!strcmp(optarg, "ctime"))
				ctl->time_format = time_format_ctime;
			else
				error(1, 0, "invalid time-format argument: %s",
				      optarg);
			break;
		case 'I':
			ctl->time_format = time_format_iso;
			break;
		case 'V':
			printf(IPUTILS_VERSION("clockdiff"));
			print_config();
			exit(0);
		case 'h':
			usage(0);
			abort();
		default:
			printf("Try '%s --help' for more information.\n",
			       program_invocation_short_name);
			exit(1);
		}
}

int main(int argc, char **argv)
{
	struct run_state ctl = {
		.rtt = 1000,
		.time_format = time_format_ctime
	};
	int measure_status;

	struct addrinfo hints = {
		.ai_family = AF_INET,
		.ai_socktype = SOCK_RAW,
		.ai_flags = AI_CANONNAME
	};
	struct addrinfo *result;
	int status;

#ifdef ENABLE_NLS
	setlocale(LC_ALL, "");
	bindtextdomain (PACKAGE_NAME, LOCALEDIR);
	textdomain (PACKAGE_NAME);
#endif

	atexit(close_stdout);

	parse_opts(&ctl, argc, argv);
	argc -= optind;
	argv += optind;
	if (argc != 1)
		usage(1);

	ctl.sock_raw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
	if (ctl.sock_raw < 0)
		error(1, errno, "socket");
	if (nice(-16) == -1)
		error(1, errno, "nice");
	drop_rights();

	if (isatty(fileno(stdin)) && isatty(fileno(stdout)))
		ctl.interactive = 1;

	ctl.id = getpid();

	status = getaddrinfo(argv[0], NULL, &hints, &result);
	if (status)
		error(1, 0, "%s: %s", argv[0], gai_strerror(status));
	ctl.hisname = strdup(result->ai_canonname);

	memcpy(&ctl.server, result->ai_addr, sizeof ctl.server);
	freeaddrinfo(result);

	if (connect(ctl.sock_raw, (struct sockaddr *)&ctl.server, sizeof(ctl.server)) == -1)
		error(1, errno, "connect");
	if (ctl.ip_opt_len) {
		struct sockaddr_in myaddr = { 0 };
		socklen_t addrlen = sizeof(myaddr);
		uint8_t *rspace;

		if ((rspace = calloc(ctl.ip_opt_len, sizeof(uint8_t))) == NULL)
			error(1, errno, "allocating %zu bytes failed",
					ctl.ip_opt_len * sizeof(uint8_t));
		rspace[0] = IPOPT_TIMESTAMP;
		rspace[1] = ctl.ip_opt_len;
		rspace[2] = 5;
		rspace[3] = IPOPT_TS_PRESPEC;
		if (getsockname(ctl.sock_raw, (struct sockaddr *)&myaddr, &addrlen) == -1)
			error(1, errno, "getsockname");
		((uint32_t *) (rspace + 4))[0 * 2] = myaddr.sin_addr.s_addr;
		((uint32_t *) (rspace + 4))[1 * 2] = ctl.server.sin_addr.s_addr;
		((uint32_t *) (rspace + 4))[2 * 2] = myaddr.sin_addr.s_addr;
		if (ctl.ip_opt_len == 4 + 4 * 8) {
			((uint32_t *) (rspace + 4))[2 * 2] = ctl.server.sin_addr.s_addr;
			((uint32_t *) (rspace + 4))[3 * 2] = myaddr.sin_addr.s_addr;
		}

		if (setsockopt(ctl.sock_raw, IPPROTO_IP, IP_OPTIONS, rspace, ctl.ip_opt_len) < 0) {
			error(0, errno, "IP_OPTIONS (fallback to icmp tstamps)");
			ctl.ip_opt_len = 0;
		}
		free(rspace);
	}

	measure_status = measure(&ctl);
	if (measure_status < 0) {
		if (errno)
			error(1, errno, "measure");
		error(1, 0, _("measure: unknown failure"));
	}

	switch (measure_status) {
	case HOSTDOWN:
		error(1, 0, _("%s is down"), ctl.hisname);
		break;
	case NONSTDTIME:
		error(1, 0, _("%s time transmitted in a non-standard format"), ctl.hisname);
		break;
	case UNREACHABLE:
		error(1, 0, _("%s is unreachable"), ctl.hisname);
		break;
	default:
		break;
	}

	{
		time_t now = time(NULL);

		if (ctl.interactive) {
			char s[32];
			struct tm tm;
			localtime_r(&now, &tm);

			if (ctl.time_format == time_format_iso)
				strftime(s, sizeof(s), "%Y-%m-%dT%H:%M:%S%z", &tm);
			else
				strftime(s, sizeof(s), "%a %b %e %H:%M:%S %Y", &tm);

			printf(_("\nhost=%s rtt=%ld(%ld)ms/%ldms delta=%dms/%dms %s\n"),
				ctl.hisname, ctl.rtt, ctl.rtt_sigma, ctl.min_rtt,
				ctl.measure_delta, ctl.measure_delta1, s);
		} else {
			printf("%lld %d %d\n", (long long)now, ctl.measure_delta, ctl.measure_delta1);
		}
	}
	exit(0);
}
