/* Pi-hole: A black hole for Internet advertisements
*  (c) 2020 Pi-hole, LLC (https://pi-hole.net)
*  Network-wide ad blocking via your own hardware.
*
*  FTL Engine
*  EDNS parsing routines
*
*  This file is copyright under the latest version of the EUPL.
*  Please see LICENSE file for your rights under this license. */

#define FTLDNS
#include "dnsmasq/dnsmasq.h"
#undef __USE_XOPEN
#include "FTL.h"
#include "log.h"
#include "edns0.h"
#include "config.h"
#include "datastructure.h"
#include "shmem.h"

// EDNS(0) Client Subnet [Optional, RFC7871]
#define EDNS0_ECS EDNS0_OPTION_CLIENT_SUBNET

// EDNS(0) COOKIE [Standard, RFC7873]
#define EDNS0_COOKIE 10

// EDNS(0) MAC address [NOT STANDARDIZED]
//
// BYTE encoding, payload size: 6 bytes
// dnsmasq option: --add-mac
#define EDNS0_MAC_ADDR_BYTE EDNS0_OPTION_MAC
// TEXT encoding, payload size: 17 bytes
// dnsmasq option: --add-mac=text
#define EDNS0_MAC_ADDR_TEXT EDNS0_OPTION_NOMDEVICEID
// BASE64 encoding, payload size: 8 bytes
// dnsmasq option: --add-mac=base64
#define EDNS0_MAC_ADDR_BASE64 EDNS0_MAC_ADDR_TEXT

// EDNS(0) CPE-ID (Common Platform Enumeration Identifier) [NOT STANDARDIZED]
// Payload: String of any length (can be zero)
// dnsmasq option: --add-cpe-id=...
#define EDNS0_CPE_ID EDNS0_OPTION_NOMCPEID

void FTL_parse_pseudoheaders(struct dns_header *header, size_t n, union mysockaddr *peer, ednsData *edns)
{
	int is_sign;
	size_t plen;
	unsigned char *pheader, *sizep;

	// Extract additional record A.K.A. pseudoheader
	if (!(pheader = find_pseudoheader(header, n, &plen, &sizep, &is_sign, NULL)))
		return;

	// Debug logging
	if(config.debug & DEBUG_EDNS0)
		for(unsigned int i = 0; i < plen; i++)
			logg("EDNS(0) pheader[%i] = 0x%02x", i, pheader[i]);

        // Working pointer
	unsigned char *p = pheader;

// RFC 6891                   EDNS(0) Extensions                   6.1.2.  Wire Format
// 
//    An OPT RR has a fixed part and a variable set of options expressed as
//    {attribute, value} pairs.  The fixed part holds some DNS metadata,
//    and also a small collection of basic extension elements that we
//    expect to be so popular that it would be a waste of wire space to
//    encode them as {attribute, value} pairs.
// 
//    The fixed part of an OPT RR is structured as follows:
// 
//        +------------+--------------+------------------------------+
//        | Field Name | Field Type   | Description                  |
//        +------------+--------------+------------------------------+
//        | NAME       | domain name  | MUST be 0 (root domain)      |
	if(*p++ != 0)
		return;
//        +------------+--------------+------------------------------+
//        | TYPE       | u_int16_t    | OPT (41)                     |
	unsigned short type;
	GETSHORT(type, p);
	if(type != 41)
		return;
//        +------------+--------------+------------------------------+
//        | CLASS      | u_int16_t    | requestor's UDP payload size |
	unsigned short class;
	GETSHORT(class, p);
	if(config.debug & DEBUG_EDNS0)
		logg("EDNS(0) requestor's UDP payload size: %u bytes", class);
//        +------------+--------------+------------------------------+
//        | TTL        | u_int32_t    | extended RCODE and flags     |
	unsigned long ttl;
	GETLONG(ttl, p);
//        +------------+--------------+------------------------------+
//        | RDLEN      | u_int16_t    | length of all RDATA          |
	unsigned short rdlen;
	GETSHORT(rdlen, p);
//        +------------+--------------+------------------------------+
//        | RDATA      | octet stream | {attribute,value} pairs      |
//        +------------+--------------+------------------------------+

//   The variable part of an OPT RR may contain zero or more options in
//   the RDATA.  Each option MUST be treated as a bit field.  Each option
//   is encoded as:
//
//                  +0 (MSB)                            +1 (LSB)
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
//    0: |                          OPTION-CODE                          |
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
//    2: |                         OPTION-LENGTH                         |
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
//    4: |                                                               |
//       /                          OPTION-DATA                          /
//       /                                                               /
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

// RFC 6891                   EDNS(0) Extensions                   6.1.3.  OPT Record TTL Field Use
//
//   The extended RCODE and flags, which OPT stores in the RR Time to Live
//   (TTL) field, are structured as follows:
//
//                  +0 (MSB)                            +1 (LSB)
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
//    0: |         EXTENDED-RCODE        |            VERSION            |
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
//    2: | DO|                           Z                               |
//       +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
//
//   EXTENDED-RCODE
//      Forms the upper 8 bits of extended 12-bit RCODE (together with the
//      4 bits defined in [RFC1035].  Note that EXTENDED-RCODE value 0
//      indicates that an unextended RCODE is in use (values 0 through
//      15).
//
//   VERSION
//      Indicates the implementation level of the setter.  Full
//      conformance with this specification is indicated by version '0'.
//      Requestors are encouraged to set this to the lowest implemented
//      level capable of expressing a transaction, to minimise the
//      responder and network load of discovering the greatest common
//      implementation level between requestor and responder.  A
//      requestor's version numbering strategy MAY ideally be a run-time
//      configuration option.
//      If a responder does not implement the VERSION level of the
//      request, then it MUST respond with RCODE=BADVERS.  All responses
//      MUST be limited in format to the VERSION level of the request, but
//      the VERSION of each response SHOULD be the highest implementation
//      level of the responder.  In this way, a requestor will learn the
//      implementation level of a responder as a side effect of every
//      response, including error responses and including RCODE=BADVERS.
	unsigned char edns0_version = (ttl >> 16) % 0xFF;
	if(edns0_version != 0x00)
		return;

	size_t offset; // The header is 11 bytes before the beginning of OPTION-DATA
	while ((offset = (p - pheader - 11u)) < rdlen && rdlen < UINT16_MAX)
	{
		unsigned short code, optlen;
		GETSHORT(code, p);
		GETSHORT(optlen, p);
		offset += 4;

		// Avoid buffer overflow due to an malicious packet
		if(offset + optlen > rdlen)
		{
			if(config.debug & DEBUG_EDNS0)
				logg("Found malicious EDNS payload, skipping record.");
			break;
		}

		// Debug logging
		if(config.debug & DEBUG_EDNS0)
			logg("EDNS(0) code %u, optlen %u (bytes %zu - %zu of %u)",
			     code, optlen, offset, offset + optlen, rdlen);

		if (code == EDNS0_ECS && config.edns0_ecs)
		{
			// EDNS(0) CLIENT SUBNET
			// RFC 7871              Client Subnet in DNS Queries              6.  Option Format
			//   This protocol uses an EDNS0 [RFC6891] option to include client
			//   address information in DNS messages.  The option is structured as
			//   follows:
			//
			//                +0 (MSB)                            +1 (LSB)
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			//   0: |                          OPTION-CODE                          |
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			//   2: |                         OPTION-LENGTH                         |
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			//   4: |                            FAMILY                             |
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			short family;
			GETSHORT(family, p);
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			//   6: |     SOURCE PREFIX-LENGTH      |     SCOPE PREFIX-LENGTH       |
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			unsigned char source_netmask = *p++;
			p++; // We are not interested in the scope prefix-length. It MUST be 0 in queries
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			//   8: |                           ADDRESS...                          /
			//      +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
			union all_addr addr = {{ 0 }};
			const size_t addrlen = optlen - 4;
			if(family == 1 && addrlen <= sizeof(addr.addr4.s_addr)) // IPv4
				memcpy(&addr.addr4.s_addr, p, addrlen);
			else if(family == 2 && addrlen <= sizeof(addr.addr6.s6_addr)) // IPv6
				memcpy(addr.addr6.s6_addr, p, addrlen);
			else
				continue;

			// Advance working pointer (we already walked 4 bytes above)
			p += optlen - 4;

			char ipaddr[ADDRSTRLEN] = { 0 };
			inet_ntop(family == 1 ? AF_INET : AF_INET6, &addr.addr4.s_addr, ipaddr, sizeof(ipaddr));

			// Only use /32 (IPv4) and /128 (IPv6) addresses
			if(!(family == 1 && source_netmask == 32) &&
			   !(family == 2 && source_netmask == 128))
			{
				if(config.debug & DEBUG_EDNS0)
					logg("EDNS(0) CLIENT SUBNET: %s/%u found (IPv%u)",
					     ipaddr, source_netmask, family == 1 ? 4 : 6);
				continue;
			}

			// Copy data to edns struct
			strncpy(edns->client, ipaddr, ADDRSTRLEN);
			edns->client[ADDRSTRLEN-1] = '\0';

			// Only set the address as useful when it is not the
			// loopback address of the distant machine (127.0.0.0/8 or ::1)
			if((family == 1 && (ntohl(addr.addr4.s_addr) & 0xFF000000) == 0x7F000000) ||
			   (family == 2 && IN6_IS_ADDR_LOOPBACK(&addr.addr6)))
			{
				if(config.debug & DEBUG_EDNS0)
					logg("EDNS(0) CLIENT SUBNET: Skipped %s/%u (IPv%u loopback address)",
					     ipaddr, source_netmask, family == 1 ? 4 : 6);
			}
			else
			{
				edns->client_set = true;
				if(config.debug & DEBUG_EDNS0)
					logg("EDNS(0) CLIENT SUBNET: %s/%u - OK (IPv%u)",
					     ipaddr, source_netmask, family == 1 ? 4 : 6);
			}
		}
		else if(code == EDNS0_COOKIE && optlen == 8)
		{
			// EDNS(0) COOKIE client
			unsigned char client_cookie[8];
			memcpy(client_cookie, p, 8);
			if(config.debug & DEBUG_EDNS0)
			{
				char pretty_client_cookie[8*2 + 1]; // client: fixed length
				char *pp = pretty_client_cookie;
				for(unsigned int j = 0; j < 8; j++)
					pp += sprintf(pp, "%02X", client_cookie[j]);
				logg("EDNS(0) COOKIE (client-only): %s",
				     pretty_client_cookie);
			}

			// Advance working pointer
			p += 8;
		}
		else if(code == EDNS0_COOKIE && optlen >= 16 && optlen <= 40)
		{
			// EDNS(0) COOKIE client + server
			unsigned char client_cookie[8];
			memcpy(client_cookie, p, 8);

			unsigned short server_cookie_len = optlen - 8;
			unsigned char server_cookie[server_cookie_len];
			memcpy(server_cookie, p + 8u, server_cookie_len);
			if(config.debug & DEBUG_EDNS0)
			{
				char pretty_client_cookie[8*2 + 1]; // client: fixed length
				char *pp = pretty_client_cookie;
				for(unsigned int j = 0; j < 8; j++)
					pp += sprintf(pp, "%02X", client_cookie[j]);
				char pretty_server_cookie[server_cookie_len*2 + 1u]; // server: variable length
				pp = pretty_server_cookie;
				for(unsigned int j = 0; j < server_cookie_len; j++)
					pp += sprintf(pp, "%02X", server_cookie[j]);
				logg("EDNS(0) COOKIE (client + server): %s (client), %s (server, %u bytes)",
				     pretty_client_cookie, pretty_server_cookie, server_cookie_len);
			}

			// Advance working pointer
			p += optlen;
		}
		else if(code == EDNS0_MAC_ADDR_BYTE && optlen == 6)
		{
			// EDNS(0) MAC address (BYTE format)
			memcpy(edns->mac_byte, p, sizeof(edns->mac_byte));
			print_mac(edns->mac_text, (unsigned char*)edns->mac_byte, sizeof(edns->mac_byte));
			edns->mac_set = true;
			if(config.debug & DEBUG_EDNS0)
				logg("EDNS(0) MAC address (BYTE format): %s", edns->mac_text);

			// Advance working pointer
			p += 6;
		}
		else if(code == EDNS0_MAC_ADDR_TEXT && optlen == 17)
		{
			// EDNS(0) MAC address (TEXT format)
			memcpy(edns->mac_text, p, 17);
			edns->mac_text[17] = '\0';
			if(sscanf(edns->mac_text, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
			          &edns->mac_byte[0],
			          &edns->mac_byte[1],
			          &edns->mac_byte[2],
			          &edns->mac_byte[3],
			          &edns->mac_byte[4],
			          &edns->mac_byte[5]) == 6)
			{
				edns->mac_set = true;
				if(config.debug & DEBUG_EDNS0)
					logg("EDNS(0) MAC address (TEXT format): %s", edns->mac_text);
			}
			else if(config.debug & DEBUG_EDNS0)
			{
				logg("         Received MAC address has invalid format!");
			}

			// Advance working pointer
			p += 17;
		}
		else if(code == EDNS0_MAC_ADDR_BASE64 && optlen == 8)
		{
			// EDNS(0) MAC address (BASE format)
			if(config.debug & DEBUG_EDNS0)
				logg("EDNS(0) MAC address (BASE64 format): NOT IMPLEMENTED");

			// Advance working pointer
			p += 8;
		}
		else if(code == EDNS0_CPE_ID && optlen < 256)
		{
			// EDNS(0) CPE-ID, 256 byte arbitrary limit
			unsigned char payload[optlen + 1u]; // variable length
			memcpy(payload, p, optlen);
			payload[optlen] = '\0';
			if(config.debug & DEBUG_EDNS0)
			{
				char pretty_payload[optlen*5 + 1u];
				char *pp = pretty_payload;
				for(unsigned int j = 0; j < optlen; j++)
					pp += sprintf(pp, "0x%02X ", payload[j]);
				pretty_payload[optlen*5 - 1] = '\0'; // Truncate away the trailing whitespace
				logg("EDNS(0) CPE-ID (payload size %u): \"%s\" (%s)",
				     optlen, payload, pretty_payload);
			}

			// Advance working pointer
			p += optlen;
		}
		else
		{
			if(config.debug & DEBUG_EDNS0)
				logg("EDNS(0):n option %u with length %u", code, optlen);
			// Not implemented, skip this record

			// Advance working pointer
			p += optlen;
		}
	}
}