/*
 * niq.c -- ICMPv6 Node Information (RFC 4620) query sender/receiver.
 * DF-2609 PoC: proves the default-enabled (icmp6_nodeinfo=3) KAME nodeinfo
 * responder answers unauthenticated queries with the system hostname
 * (QTYPE FQDN, no-subject oldfqdn compat path) and the full IPv6 address
 * inventory (QTYPE NODEADDR + ALL/scope flags).
 *
 * Query wire format (after the IPv6 header the kernel prepends):
 *   type=139 code=0 cksum=0 qtype(BE16) flags(BE16) nonce[8] [subject(16)]
 *
 * The kernel computes the ICMPv6 checksum for raw ICMPv6 sockets
 * (raw_ip6.c:393-418), so cksum is sent as 0.
 *
 * usage: niq [-r repeats] dst fqdn|nodeaddr|suptypes [flags-be-hex]
 *   flags-be-hex example for nodeaddr: 002e  (ALL|COMPAT|LINKLOCAL|GLOBAL)
 *   default nodeaddr flags: 002e
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define NI_QUERY_TYPE   139
#define NI_REPLY_TYPE   140
#define QTYPE_SUPTYPES  1
#define QTYPE_FQDN      2
#define QTYPE_NODEADDR  3

struct niq_hdr {
	uint8_t  type;
	uint8_t  code;
	uint16_t cksum;
	uint16_t qtype;		/* BE on the wire */
	uint16_t flags;		/* BE on the wire */
	uint8_t  nonce[8];
} __attribute__((__packed__));

static void
hexdump(const uint8_t *b, size_t n, const char *pfx)
{
	size_t i;

	for (i = 0; i < n; i++) {
		if (i % 16 == 0)
			printf("%s%04zx:", pfx, i);
		printf(" %02x", b[i]);
		if (i % 16 == 15 || i + 1 == n)
			printf("\n");
	}
}

static const char *
scope_of(const struct in6_addr *a)
{
	if (IN6_IS_ADDR_UNSPECIFIED(a))		return "unspecified";
	if (IN6_IS_ADDR_LOOPBACK(a))		return "loopback";
	if (IN6_IS_ADDR_LINKLOCAL(a))		return "linklocal";
	if (IN6_IS_ADDR_SITELOCAL(a))		return "sitelocal";
	if (IN6_IS_ADDR_V4COMPAT(a))		return "v4compat";
	if (IN6_IS_ADDR_V4MAPPED(a))		return "v4mapped";
	if (IN6_IS_ADDR_MULTICAST(a))		return "multicast";
	return "global";
}

static void
parse_reply(const uint8_t *pkt, ssize_t n, int skip)
{
	const struct niq_hdr *h = (const void *)(pkt + skip);
	const uint8_t *payload;
	size_t plen, off;
	char ip[64];

	if (n < skip + (ssize_t)sizeof(*h)) {
		printf("PARSE: short reply (%zd bytes)\n", n);
		return;
	}
	plen = n - skip - sizeof(*h);
	payload = (const uint8_t *)h + sizeof(*h);

	if (skip)
		inet_ntop(AF_INET6, pkt + 24, ip, sizeof(ip));
	else
		snprintf(ip, sizeof(ip), "(raw)");
	printf("REPLY from %s: type=%u code=%u qtype=%u flags=0x%04x nonce="
	       "%02x%02x%02x%02x%02x%02x%02x%02x payload=%zu\n",
	       ip, h->type, h->code, ntohs(h->qtype), ntohs(h->flags),
	       h->nonce[0], h->nonce[1], h->nonce[2], h->nonce[3],
	       h->nonce[4], h->nonce[5], h->nonce[6], h->nonce[7], plen);

	switch (ntohs(h->qtype)) {
	case QTYPE_FQDN: {
		const uint8_t *p;
		size_t i, l;

		if (plen < 4) {
			printf("FQDN: no TTL/data\n");
			break;
		}
		printf("FQDN ttl=0x%02x%02x%02x%02x name=", 
		       payload[0], payload[1], payload[2], payload[3]);
		p = payload + 4;
		i = 0;
		while (i < plen - 4 && (l = p[i]) != 0) {
			if (i) printf(".");
			if (i + 1 + l > plen - 4) {
				printf("<bogus len %zu>", l);
				break;
			}
			fwrite(p + i + 1, 1, l, stdout);
			i += 1 + l;
		}
		printf("\n");
		break;
	}
	case QTYPE_NODEADDR: {
		size_t rec;

		if (plen % 20)
			printf("NODEADDR: payload not multiple of 20 (%zu)\n",
			       plen);
		printf("NODEADDR count=%zu\n", plen / 20);
		for (rec = 0; rec + 20 <= plen; rec += 20) {
			const uint8_t *r = payload + rec;
			struct in6_addr a;
			uint32_t aflags;

			memcpy(&a, r + 4, 16);
			aflags = ((uint32_t)r[0] << 24) | ((uint32_t)r[1] << 16) |
			    ((uint32_t)r[2] << 8) | r[3];
			inet_ntop(AF_INET6, &a, ip, sizeof(ip));
			printf("NODEADDR[%zu] flags=0x%08x %s (%s)\n",
			       rec / 20, aflags, ip, scope_of(&a));
		}
		break;
	}
	case QTYPE_SUPTYPES:
		printf("SUPTYPES bitmap=0x");
		for (off = 0; off < plen && off < 4; off++)
			printf("%02x", payload[off]);
		printf("\n");
		break;
	default:
		break;
	}
}

int
main(int argc, char **argv)
{
	struct sockaddr_in6 dst, from;
	socklen_t fromlen;
	struct icmp6_filter filt;
	struct niq_hdr q;
	uint8_t buf[2048];
	uint8_t pkt[64];
	int s, r, repeats = 1, i, opt, qtype = QTYPE_FQDN, rv = 1;
	unsigned long flags = 0x2e;	/* ALL|COMPAT|LINKLOCAL|GLOBAL */
	struct timeval tv;
	ssize_t n;

	while ((opt = getopt(argc, argv, "r:")) != -1) {
		switch (opt) {
		case 'r':
			repeats = atoi(optarg);
			break;
		default:
			goto usage;
		}
	}
	argc -= optind;
	argv += optind;
	if (argc < 2)
		goto usage;

	memset(&dst, 0, sizeof(dst));
	dst.sin6_family = AF_INET6;
	if (inet_pton(AF_INET6, argv[0], &dst.sin6_addr) != 1) {
		fprintf(stderr, "bad dst %s\n", argv[0]);
		return 2;
	}
	if (!strcmp(argv[1], "fqdn"))
		qtype = QTYPE_FQDN;
	else if (!strcmp(argv[1], "nodeaddr"))
		qtype = QTYPE_NODEADDR;
	else if (!strcmp(argv[1], "suptypes"))
		qtype = QTYPE_SUPTYPES;
	else
		goto usage;
	if (argc > 2)
		flags = strtoul(argv[2], NULL, 16);

	s = socket(AF_INET6, SOCK_RAW, 58 /* IPPROTO_ICMPV6 */);
	if (s < 0) {
		perror("socket(SOCK_RAW, ICMPV6) [needs root]");
		return 2;
	}
	ICMP6_FILTER_SETBLOCKALL(&filt);
	ICMP6_FILTER_SETPASS(NI_REPLY_TYPE, &filt);
	if (setsockopt(s, IPPROTO_ICMPV6, ICMP6_FILTER, &filt, sizeof(filt)))
		perror("setsockopt(ICMP6_FILTER)");
	i = 255;
	setsockopt(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &i, sizeof(i));

	for (r = 0; r < repeats; r++) {
		memset(&q, 0, sizeof(q));
		q.type = NI_QUERY_TYPE;
		q.code = 0;		/* no subject / old format */
		q.cksum = 0;
		q.qtype = htons(qtype);
		q.flags = htons(flags);
		for (i = 0; i < 8; i++)
			q.nonce[i] = (getpid() >> (i % 8)) ^ (r + 1) ^ 0xa5;

		memset(pkt, 0, sizeof(pkt));
		memcpy(pkt, &q, sizeof(q));
		if (qtype == QTYPE_NODEADDR) {
			/* ni6_input requires a 16-byte subject == dst (code 0) */
			memcpy(pkt + 16, &dst.sin6_addr, 16);
		}

		printf("=== query %d -> %s qtype=%u flags=0x%04lx len=%d\n",
		       r + 1, argv[0], qtype, flags,
		       (int)sizeof(q) + (qtype == QTYPE_NODEADDR ? 16 : 0));
		if (sendto(s, pkt, sizeof(q) +
		    (qtype == QTYPE_NODEADDR ? 16 : 0), 0,
		    (struct sockaddr *)&dst, sizeof(dst)) < 0) {
			perror("sendto");
			goto out;
		}

		tv.tv_sec = 2;
		tv.tv_usec = 0;
		setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
		fromlen = sizeof(from);
		n = recvfrom(s, buf, sizeof(buf), 0,
		    (struct sockaddr *)&from, &fromlen);
		if (n < 0) {
			if (errno == EAGAIN || errno == EWOULDBLOCK)
				printf("NO REPLY (timeout)\n");
			else
				perror("recvfrom");
			continue;
		}
		hexdump(buf, n, "  ");
		/* raw v6 ICMP sockets deliver the ICMPv6 message without
		 * the IPv6 header; auto-detect */
		{
			int skip = (buf[0] == NI_REPLY_TYPE ||
			    buf[0] == NI_QUERY_TYPE) ? 0 : 40;
			parse_reply(buf, n, skip);
		}
		rv = 0;	/* got at least one reply */
	}
out:
	close(s);
	return rv;
usage:
	fprintf(stderr,
	    "usage: niq [-r repeats] dst fqdn|nodeaddr|suptypes [flags-be-hex]\n");
	return 2;
}
