/* send out an IKE "ping" packet.
 * Copyright (C) 2002 Michael Richardson
 * Copyright (C) 2002 D. Hugh Redelmeier.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 *
 * RCSID $Id: ikeping.c,v 1.13.2.2 2007-04-25 17:43:36 paul Exp $
 */

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <getopt.h>
#include <assert.h>
#include <poll.h>

#include <openswan.h>
#include "pfkeyv2.h"

#include "constants.h"
#include "packet.h"

#include "natt_defines.h"

/* what exchange number to use for outgoing requests */
static int exchange_number;

static void
help(void)
{
    fprintf(stderr,
	"Usage:\n\n"
	"ikeping"
	    " [--listen]     causes IKEping to open a socket and reply to requests.\n"
	    " [--verbose]    causes IKEping to hexdump all packets sent/received.\n"
	    " [--ikeport <port-number>]      port to listen on/send from\n"
	    " [--ikeaddress <address>]       address to listen on/send from\n"
	    " [--inet]       just send/listen on IPv4 socket\n"
	    " [--inet6]      just send/listen on IPv6 socket\n"
	    " [--version]    just dump version number and exit\n"
	    " [--nat-t]      enabled NONESP encapsulation on port\n"
	    " [--exchangenum num]    use num instead of 244 for the exchange type.\n"
	    " [--wait seconds]    time to wait for replies, defaults to 10 seconds.\n"
	    " host/port ...\n\n"
	"Openswan %s\n",
	ipsec_version_code());
}

static void
hton_ping(struct isakmp_hdr *ih)
{
	u_int32_t *ihp;

	ihp=(u_int32_t *)ih;

	/* put it in network byte order. */
	/* cookies are byte viewed anyway */
	ihp[4]=htonl(ihp[4]);
	ih->isa_msgid  = htonl(ih->isa_msgid);
	ih->isa_length = htonl(ih->isa_length);
}

static void
ntoh_ping(struct isakmp_hdr *ih)
{
	u_int32_t *ihp;

	ihp=(u_int32_t *)ih;

	/* put it in network byte order. */
	/* cookies are byte viewed anyway */
	ihp[4]=ntohl(ihp[4]);
	ih->isa_msgid  = ntohl(ih->isa_msgid);
	ih->isa_length = ntohl(ih->isa_length);
}


/*
 * send an IKE ping
 *
 */
static void
send_ping(int afamily,
	  int s,
	  ip_address *raddr,
	  int rport)
{
	struct isakmp_hdr ih;
	int i, raddrlen;

	raddrlen=0;

	for(i=0; i<COOKIE_SIZE; i++) {
		ih.isa_icookie[i]=rand()&0xff;		
	}
	     
	for(i=0; i<COOKIE_SIZE; i++) {
		ih.isa_rcookie[i]=rand()&0xff;		
	}
	     
	ih.isa_np    = NOTHING_WRONG;
	ih.isa_version = (1 << ISA_MAJ_SHIFT) | 0;
	ih.isa_xchg  = (exchange_number ?
			exchange_number : ISAKMP_XCHG_ECHOREQUEST_PRIVATE);
	ih.isa_flags =0;
	ih.isa_msgid =rand();
	ih.isa_length=0;

	switch(afamily) {
	case AF_INET:
		raddr->u.v4.sin_port = htons(rport);
		raddrlen=sizeof(raddr->u.v4);
		break;
	  
	case AF_INET6:
		raddr->u.v6.sin6_port = htons(rport);
		raddrlen=sizeof(raddr->u.v6);
		break;
	}

	hton_ping(&ih);

	if(sendto(s, &ih, sizeof(ih), 0, (struct sockaddr *)raddr, raddrlen) < 0) {
		perror("sendto");
		exit(5);
	}
}

/*
 * send an IKE ping
 *
 */
static void
reply_packet(int afamily,
	     int s,
	     ip_address *dst_addr,
	     int         dst_len,
	     struct isakmp_hdr *op)
{
	int i, tmp;

	tmp=afamily;  /* shut up compiler */

	for(i=0; i<COOKIE_SIZE; i++) {
		tmp=op->isa_icookie[i];
		op->isa_icookie[i]=op->isa_rcookie[i];
		op->isa_rcookie[i]=tmp;
	}
	     
	op->isa_np    = NOTHING_WRONG;
	op->isa_version = (1 << ISA_MAJ_SHIFT) | 0;
	op->isa_xchg  = ISAKMP_XCHG_ECHOREPLY;
	op->isa_flags =0;
	op->isa_msgid =rand();
	op->isa_length=0;

	hton_ping(op);

	if(sendto(s, op, sizeof(*op), 0, (struct sockaddr *)dst_addr, dst_len) < 0) {
		perror("sendto");
		exit(5);
	}
}

/*
 * receive and decode packet.
 *
 */
static void
receive_ping(int afamily, int s, int reply, int natt)
{
	ip_address sender;
	struct isakmp_hdr ih;
	char   rbuf[256];
	char   buf[64];
	int n, rport;
	unsigned int sendlen;
	const char *xchg_name;
	int xchg;

	rport = 500;
	xchg  = 0;
	sendlen=sizeof(sender);
	n = recvfrom(s, rbuf, sizeof(rbuf)
		     , 0, (struct sockaddr *)&sender, (socklen_t *)&sendlen);

	memcpy(&ih, rbuf, sizeof(ih));
	if(natt) {
	    /* need to skip 4 bytes! */
	    if(rbuf[0]!=0x0 || rbuf[1]!=0x0 ||
	       rbuf[2]!=0x0 || rbuf[3]!=0x0) {
		printf("kernel failed to steal ESP packet (SPI=0x%02x%02x%02x%02x) of length %d\n"
		       , rbuf[0], rbuf[1], rbuf[2], rbuf[3]
		       , n);
		return;
	    }
	    
	    /* otherwise, skip 4 bytes */
	    memcpy(&ih, rbuf+4, sizeof(ih));
	}
	    
	addrtot(&sender, 0, buf, sizeof(buf));
	switch(afamily) {
	case AF_INET:
		rport = sender.u.v4.sin_port;
		break;
	  
	case AF_INET6:
		rport = sender.u.v6.sin6_port;
		break;
	}

	if((unsigned int)n < sizeof(ih)) {
		fprintf(stderr, "read short packet (%d) from %s/%d\n",
			n, buf, rport);
		return;
	}

	/* translate from network byte order */
	ntoh_ping(&ih);


	if(ih.isa_xchg == ISAKMP_XCHG_ECHOREQUEST       ||
	   ih.isa_xchg == ISAKMP_XCHG_ECHOREQUEST_PRIVATE  ||
	   (exchange_number!=0 && ih.isa_xchg == exchange_number)) {
		xchg_name="echo-request";
		xchg=ISAKMP_XCHG_ECHOREQUEST;
	} else if(ih.isa_xchg == ISAKMP_XCHG_ECHOREPLY ||
		  ih.isa_xchg == ISAKMP_XCHG_ECHOREPLY_PRIVATE ||
		  (exchange_number!=0 && ih.isa_xchg == exchange_number+1)) {
		xchg_name="echo-reply";
	} else {
		xchg_name="";
	}

	printf("received %d(%s) packet from %s/%d of len: %d\n",
	       ih.isa_xchg, xchg_name, buf, ntohs(rport), n);

	printf("\trcookie=%08x_%08x icookie=%08x_%08x msgid=%08x\n",
	       *(u_int32_t *)(ih.isa_icookie), 
	       *(u_int32_t *)(ih.isa_icookie+4), 
	       *(u_int32_t *)(ih.isa_rcookie), 
	       *(u_int32_t *)(ih.isa_rcookie+4),
	       ih.isa_msgid);
	printf("\tnp=%03d  version=%d.%d    xchg=%s(%d)\n",
	       ih.isa_np,
	       ih.isa_version >> ISA_MAJ_SHIFT,
	       ih.isa_version & ISA_MIN_MASK,
	       xchg_name,
	       ih.isa_xchg);

	if(reply && xchg==ISAKMP_XCHG_ECHOREQUEST) {
		reply_packet(afamily, s, &sender, sendlen, &ih);
	}
}

static const struct option long_opts[] = {
    /* name, has_arg, flag, val */
    { "help",        no_argument, NULL, 'h' },
    { "version",     no_argument, NULL, 'V' },
    { "verbose",     no_argument, NULL, 'v' },
    { "listen",      no_argument, NULL, 's' },
    { "ikeport",     required_argument, NULL, 'p' },
    { "ikeaddress",  required_argument, NULL, 'b' },
    { "inet",        no_argument, NULL, '4' },
    { "inet6",       no_argument, NULL, '6' },
    { "nat-t",       no_argument, NULL, 'T' },
    { "natt",        no_argument, NULL, 'T' },
    { "exchangenum", required_argument, NULL, 'n' },
    { "wait",        required_argument, NULL, 'w' },
    { 0,0,0,0 }
};

int
main(int argc, char **argv)
{
  char *foo;
  const char *errstr;
  int   s;
  int   listen_only;
  int   lport,dport;
  int   afamily;
  int   pfamily;
  int   c;
  int   numSenders, numReceived, noDNS;
  int   natt;
  int   waitTime;
  int   verbose, timedOut;
  ip_address laddr, raddr;
  char *afam = "";

  afamily=AF_INET;
  pfamily=PF_INET;
  lport=500;
  dport=500;
  waitTime=10*1000;
  verbose=0;
  natt=0;
  listen_only=0;
  noDNS=0;
  bzero(&laddr, sizeof(laddr));

  while((c = getopt_long(argc, argv, "hVnvsp:b:46E:w:", long_opts, 0))!=EOF) {
      switch (c) {
      case 'h':	        /* --help */
	  help();
	  return 0;	/* GNU coding standards say to stop here */
	  
      case 'V':               /* --version */
	  fprintf(stderr, "FreeS/WAN %s\n", ipsec_version_code());
	  return 0;	/* GNU coding standards say to stop here */
	  
      case 'v':	/* --label <string> */
	  verbose++;
	  continue;
	  
      case 'n':
	  noDNS=1;
	  break;
	  
      case 'T':
	  natt++;
	  break;
	  
      case 'E':
	  exchange_number=strtol(optarg, &foo, 0);
	  if(optarg==foo || exchange_number < 1 || exchange_number>255) {
	      fprintf(stderr, "Invalid exchange number '%s' (should be 1<=x<255)\n",
		      optarg);
	      exit(1);
	  }
	  continue;
	  
	  
      case 's':
	  listen_only++;
	  continue;
	  
      case 'p':
	  lport=strtol(optarg, &foo, 0);
	  if(optarg==foo || lport <0 || lport>65535) {
	      fprintf(stderr, "Invalid port number '%s' (should be 0<=x<65536)\n",
		      optarg);
	      exit(1);
	  }
	  continue;
	  
      case 'w':
	  waitTime=strtol(optarg, &foo, 0);
	  if(optarg==foo || waitTime < 0) {
	      fprintf(stderr, "Invalid waittime number '%s' (should be 0<=x)\n",
		      optarg);
	      exit(1);
	  }
	  continue;
	  
      case 'b':
	  errstr = ttoaddr(optarg, strlen(optarg), afamily, &laddr);
	  if(errstr!=NULL) {
	      fprintf(stderr, "Invalid local address '%s': %s\n",
		      optarg, errstr);
	      exit(1);
	  }
	  continue;
	  
      case '4':
	  afamily=AF_INET;
	  pfamily=PF_INET;
	  afam = "IPv4";
	  continue;
	  
      case '6':
	  afamily=AF_INET6;
	  pfamily=PF_INET6;
	  afam = "IPv6";
	  continue;
	  
      default:
	  assert(FALSE);	/* unknown return value */
      }
  }

  s=socket(pfamily, SOCK_DGRAM, IPPROTO_UDP);
  if(s < 0) {
    perror("socket");
    exit(3);
  }

  switch(afamily) {
  case AF_INET:
	  laddr.u.v4.sin_port = htons(lport);
	  if(bind(s, (struct sockaddr *)&laddr.u.v4, sizeof(laddr.u.v4)) < 0) {
		  perror("v4 bind");
		  exit(5);
	  }
	  break;
	  
  case AF_INET6:
	  laddr.u.v6.sin6_port = htons(lport);
	  if(bind(s, (struct sockaddr *)&laddr.u.v6, sizeof(laddr.u.v6)) < 0) {
		  perror("v6 bind");
		  exit(5);
	  }
	  break;
  }

  if(natt) {
      int r;
      
      /* only support RFC method */
      int type = ESPINUDP_WITH_NON_ESP;
      r = setsockopt(s, SOL_UDP, UDP_ESPINUDP, &type, sizeof(type));
      if ((r<0) && (errno == ENOPROTOOPT)) {
	  fprintf(stderr, 
		 "NAT-Traversal: ESPINUDP(%d) not supported by kernel for family %s"
		 , type, afam);
      }
  }

  numSenders = 0;

  if(!listen_only) {
	  while(optind < argc) {
		  char *port;
		  char *host;
		  char  namebuf[128];

		  host = argv[optind];

		  port = strchr(host, '/');
		  dport=500;
		  if(port) {
			 *port='\0';
			  port++;
			  dport= strtol(port, &foo, 0);
			  if(port==foo || dport < 0 || dport > 65535) {
				  fprintf(stderr, "Invalid port number '%s' "
					  "(should be 0<=x<65536)\n",
					  port);
				  exit(1);
			  }
		  }

		  errstr = ttoaddr(host, strlen(host),
				   afamily, &raddr);
		  if(errstr!=NULL) {
			  fprintf(stderr, "Invalid remote address '%s': %s\n",
				  host, errstr);
			  exit(1);
		  }

		  addrtot(&raddr, 0, namebuf, sizeof(namebuf));

		  printf("Sending packet to %s/%d\n", namebuf, dport);
			 
		  send_ping(afamily, s, &raddr, dport);
		  numSenders++;
		  optind++;
	  }
  }

  timedOut = 0;
  numReceived=0;

  /* really should catch ^C and print stats on exit */
  while(numSenders > 0 || listen_only) {
	  struct pollfd  ready;
	  int n;

	  ready.fd = s;
	  ready.events = POLLIN;

	  n = poll(&ready, 1, waitTime);
	  if(n < 0) {
	      if(errno != EINTR) {
		  perror("poll");
		  exit(1);
	      }
	  }
	  
	  if(n == 0 && !listen_only) {
		  break;
	  }

	  if(n == 1) {
		  numReceived++;
		  receive_ping(afamily, s, listen_only, natt);
	  }
  }

  printf("%d packets sent, %d packets received. %d%% packet loss\n",
  numSenders,
  numReceived,
  numSenders > 0 ? 100-numReceived*100/numSenders : 0);
  exit(0);
}

/*
 * Local variables:
 * c-file-style: "pluto"
 * c-basic-offset: 4
 * End:
 *
 */
