lib_resolv.c

/*
 * nslookup_lede - musl compatible replacement for busybox nslookup
 *
 * Copyright (C) 2017 Jo-Philipp Wich <jo@mein.io>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/**
 * # DNS Resolution Module
 *
 * The `resolv` module provides DNS resolution functionality for ucode, allowing
 * you to perform DNS queries for various record types and handle responses.
 *
 * Functions can be individually imported and directly accessed using the
 * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import named import}
 * syntax:
 *
 *   ```
 *   import { query } from 'resolv';
 *
 *   let result = query('example.com', { type: ['A'] });
 *   ```
 *
 * Alternatively, the module namespace can be imported
 * using a wildcard import statement:
 *
 *   ```
 *   import * as resolv from 'resolv';
 *
 *   let result = resolv.query('example.com', { type: ['A'] });
 *   ```
 *
 * Additionally, the resolv module namespace may also be imported by invoking
 * the `ucode` interpreter with the `-lresolv` switch.
 *
 * ## Record Types
 *
 * The module supports the following DNS record types:
 *
 * | Type    | Description                    |
 * |---------|--------------------------------|
 * | `A`     | IPv4 address record            |
 * | `AAAA`  | IPv6 address record            |
 * | `CNAME` | Canonical name record          |
 * | `MX`    | Mail exchange record           |
 * | `NS`    | Name server record             |
 * | `PTR`   | Pointer record (reverse DNS)   |
 * | `SOA`   | Start of authority record      |
 * | `SRV`   | Service record                 |
 * | `TXT`   | Text record                    |
 * | `ANY`   | Any available record type      |
 *
 * ## Response Codes
 *
 * DNS queries can return the following response codes:
 *
 * | Code        | Description                               |
 * |-------------|-------------------------------------------|
 * | `NOERROR`   | No error, query successful                |
 * | `FORMERR`   | Format error in query                     |
 * | `SERVFAIL`  | Server failure                            |
 * | `NXDOMAIN`  | Non-existent domain                       |
 * | `NOTIMP`    | Not implemented                           |
 * | `REFUSED`   | Query refused                             |
 * | `TIMEOUT`   | Query timed out                           |
 *
 * ## Response Format
 *
 * DNS query results are returned as objects where:
 * - Keys are the queried domain names
 * - Values are objects containing arrays of records grouped by type
 * - Special `rcode` property indicates query status for failed queries
 *
 * ### Record Format by Type
 *
 * **A and AAAA records:**
 * ```javascript
 * {
 *   "example.com": {
 *     "A": ["192.0.2.1", "192.0.2.2"],
 *     "AAAA": ["2001:db8::1", "2001:db8::2"]
 *   }
 * }
 * ```
 *
 * **MX records:**
 * ```javascript
 * {
 *   "example.com": {
 *     "MX": [
 *       [10, "mail1.example.com"],
 *       [20, "mail2.example.com"]
 *     ]
 *   }
 * }
 * ```
 *
 * **SRV records:**
 * ```javascript
 * {
 *   "_http._tcp.example.com": {
 *     "SRV": [
 *       [10, 5, 80, "web1.example.com"],
 *       [10, 10, 80, "web2.example.com"]
 *     ]
 *   }
 * }
 * ```
 *
 * **SOA records:**
 * ```javascript
 * {
 *   "example.com": {
 *     "SOA": [
 *       [
 *         "ns1.example.com",      // primary nameserver
 *         "admin.example.com",    // responsible mailbox
 *         2023010101,             // serial number
 *         3600,                   // refresh interval
 *         1800,                   // retry interval
 *         604800,                 // expire time
 *         86400                   // minimum TTL
 *       ]
 *     ]
 *   }
 * }
 * ```
 *
 * **TXT, NS, CNAME, PTR records:**
 * ```javascript
 * {
 *   "example.com": {
 *     "TXT": ["v=spf1 include:_spf.example.com ~all"],
 *     "NS": ["ns1.example.com", "ns2.example.com"],
 *     "CNAME": ["alias.example.com"]
 *   }
 * }
 * ```
 *
 * **Error responses:**
 * ```javascript
 * {
 *   "nonexistent.example.com": {
 *     "rcode": "NXDOMAIN"
 *   }
 * }
 * ```
 *
 * ## Examples
 *
 * Basic A record lookup:
 *
 * ```javascript
 * import { query } from 'resolv';
 *
 * const result = query(['example.com']);
 * print(result, "\n");
 * // {
 * //   "example.com": {
 * //     "A": ["192.0.2.1"],
 * //     "AAAA": ["2001:db8::1"]
 * //   }
 * // }
 * ```
 *
 * Specific record type query:
 *
 * ```javascript
 * const mxRecords = query(['example.com'], { type: ['MX'] });
 * print(mxRecords, "\n");
 * // {
 * //   "example.com": {
 * //     "MX": [[10, "mail.example.com"]]
 * //   }
 * // }
 * ```
 *
 * Multiple domains and types:
 *
 * ```javascript
 * const results = query(
 *   ['example.com', 'google.com'],
 *   { 
 *     type: ['A', 'MX'],
 *     timeout: 10000,
 *     nameserver: ['8.8.8.8', '1.1.1.1']
 *   }
 * );
 * ```
 *
 * Reverse DNS lookup:
 *
 * ```javascript
 * const ptrResult = query(['192.0.2.1'], { type: ['PTR'] });
 * print(ptrResult, "\n");
 * // {
 * //   "1.2.0.192.in-addr.arpa": {
 * //     "PTR": ["example.com"]
 * //   }
 * // }
 * ```
 *
 * @module resolv
 */

#include <stdio.h>
#include <resolv.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <poll.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netdb.h>
#include <fcntl.h>

#include "ucode/module.h"

#define for_each_item(arr, item) \
	for (uc_value_t *_idx = NULL, *item = (ucv_type(arr) == UC_ARRAY) ? ucv_array_get(arr, 0) : arr; \
	     (uintptr_t)_idx < (ucv_type(arr) == UC_ARRAY ? ucv_array_length(arr) : (arr != NULL)); \
	     _idx = (void *)((uintptr_t)_idx + 1), item = ucv_array_get(arr, (uintptr_t)_idx))

#define err_return(code, ...) do { set_error(code, __VA_ARGS__); return NULL; } while(0)

static struct {
	int code;
	char *msg;
} last_error;

__attribute__((format(printf, 2, 3))) static void
set_error(int errcode, const char *fmt, ...) {
	va_list ap;

	free(last_error.msg);

	last_error.code = errcode;
	last_error.msg = NULL;

	if (fmt) {
		va_start(ap, fmt);
		xvasprintf(&last_error.msg, fmt, ap);
		va_end(ap);
	}
}

typedef struct {
	socklen_t len;
	union {
		struct sockaddr sa;
		struct sockaddr_in sin;
		struct sockaddr_in6 sin6;
	} u;
} addr_t;

typedef struct {
	const char *name;
	addr_t addr;
} ns_t;

typedef struct {
	char *name;
	size_t qlen, rlen;
	unsigned char query[512];
	int rcode;
} query_t;

typedef struct __attribute__((packed)) {
	uint8_t root_domain;
	uint16_t type;
	uint16_t edns_maxsize;
	uint8_t extended_rcode;
	uint8_t edns_version;
	uint16_t z;
	uint16_t data_length;
} opt_rr_t;

typedef struct {
	uint32_t qtypes;
	size_t n_ns;
	ns_t *ns;
	size_t n_queries;
	query_t *queries;
	uint32_t retries;
	uint32_t timeout;
	uint16_t edns_maxsize;
}  resolve_ctx_t;


static struct {
	int type;
	const char *name;
} qtypes[] = {
	{ ns_t_soa,   "SOA"   },
	{ ns_t_ns,    "NS"    },
	{ ns_t_a,     "A"     },
	{ ns_t_aaaa,  "AAAA"  },
	{ ns_t_cname, "CNAME" },
	{ ns_t_mx,    "MX"    },
	{ ns_t_txt,   "TXT"   },
	{ ns_t_srv,   "SRV"   },
	{ ns_t_ptr,   "PTR"   },
	{ ns_t_any,   "ANY"   },
	{ }
};

static const char *rcodes[] = {
	"NOERROR",
	"FORMERR",
	"SERVFAIL",
	"NXDOMAIN",
	"NOTIMP",
	"REFUSED",
	"YXDOMAIN",
	"YXRRSET",
	"NXRRSET",
	"NOTAUTH",
	"NOTZONE",
	"RESERVED11",
	"RESERVED12",
	"RESERVED13",
	"RESERVED14",
	"RESERVED15",
	"BADVERS"
};

static unsigned int default_port = 53;


static uc_value_t *
init_obj(uc_vm_t *vm, uc_value_t *obj, const char *key, uc_type_t type)
{
	uc_value_t *existing;

	existing = ucv_object_get(obj, key, NULL);

	if (existing == NULL) {
		switch (type) {
		case UC_ARRAY:
			existing = ucv_array_new(vm);
			break;

		case UC_OBJECT:
			existing = ucv_object_new(vm);
			break;

		default:
			return NULL;
		}

		ucv_object_add(obj, key, existing);
	}

	return existing;
}

static int
parse_reply(uc_vm_t *vm, uc_value_t *res_obj, const unsigned char *msg, size_t len)
{
	ns_msg handle;
	ns_rr rr;
	int i, n, rdlen;
	const char *key = NULL;
	char astr[INET6_ADDRSTRLEN], dname[MAXDNAME];
	const unsigned char *cp;
	uc_value_t *name_obj, *type_arr, *item;

	if (ns_initparse(msg, len, &handle) != 0) {
		set_error(errno, "Unable to parse reply packet");

		return -1;
	}

	for (i = 0; i < ns_msg_count(handle, ns_s_an); i++) {
		if (ns_parserr(&handle, ns_s_an, i, &rr) != 0) {
			set_error(errno, "Unable to parse resource record");

			return -1;
		}

		name_obj = init_obj(vm, res_obj, ns_rr_name(rr), UC_OBJECT);

		rdlen = ns_rr_rdlen(rr);

		switch (ns_rr_type(rr))
		{
		case ns_t_a:
			if (rdlen != 4) {
				set_error(EBADMSG, "Invalid A record length");

				return -1;
			}

			type_arr = init_obj(vm, name_obj, "A", UC_ARRAY);

			inet_ntop(AF_INET, ns_rr_rdata(rr), astr, sizeof(astr));
			ucv_array_push(type_arr, ucv_string_new(astr));
			break;

		case ns_t_aaaa:
			if (rdlen != 16) {
				set_error(EBADMSG, "Invalid AAAA record length");

				return -1;
			}

			type_arr = init_obj(vm, name_obj, "AAAA", UC_ARRAY);

			inet_ntop(AF_INET6, ns_rr_rdata(rr), astr, sizeof(astr));
			ucv_array_push(type_arr, ucv_string_new(astr));
			break;

		case ns_t_ns:
			if (!key)
				key = "NS";
			/* fall through */

		case ns_t_cname:
			if (!key)
				key = "CNAME";
			/* fall through */

		case ns_t_ptr:
			if (!key)
				key = "PTR";

			if (ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
				ns_rr_rdata(rr), dname, sizeof(dname)) < 0) {
				set_error(errno, "Unable to uncompress domain name");

				return -1;
			}

			type_arr = init_obj(vm, name_obj, key, UC_ARRAY);
			n = ucv_array_length(type_arr);
			item = n ? ucv_array_get(type_arr, n - 1) : NULL;

			if (!n || strcmp(ucv_string_get(item), dname))
				ucv_array_push(type_arr, ucv_string_new(dname));

			break;

		case ns_t_mx:
			if (rdlen < 2) {
				set_error(EBADMSG, "MX record too short");

				return -1;
			}

			n = ns_get16(ns_rr_rdata(rr));

			if (ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
				ns_rr_rdata(rr) + 2, dname, sizeof(dname)) < 0) {
				set_error(errno, "Unable to uncompress MX domain");

				return -1;
			}

			type_arr = init_obj(vm, name_obj, "MX", UC_ARRAY);
			item = ucv_array_new_length(vm, 2);
			ucv_array_push(item, ucv_int64_new(n));
			ucv_array_push(item, ucv_string_new(dname));
			ucv_array_push(type_arr, item);
			break;

		case ns_t_txt:
			if (rdlen < 1) {
				set_error(EBADMSG, "TXT record too short");

				return -1;
			}

			n = *(unsigned char *)ns_rr_rdata(rr);

			if (n > 0) {
				memset(dname, 0, sizeof(dname));
				memcpy(dname, ns_rr_rdata(rr) + 1, n);

				type_arr = init_obj(vm, name_obj, "TXT", UC_ARRAY);
				ucv_array_push(type_arr, ucv_string_new(dname));
			}
			break;

		case ns_t_srv:
			if (rdlen < 6) {
				set_error(EBADMSG, "SRV record too short");

				return -1;
			}

			cp = ns_rr_rdata(rr);
			n = ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
			                       cp + 6, dname, sizeof(dname));

			if (n < 0) {
				set_error(errno, "Unable to uncompress domain name");

				return -1;
			}

			type_arr = init_obj(vm, name_obj, "SRV", UC_ARRAY);
			item = ucv_array_new_length(vm, 4);
			ucv_array_push(item, ucv_int64_new(ns_get16(cp)));
			ucv_array_push(item, ucv_int64_new(ns_get16(cp + 2)));
			ucv_array_push(item, ucv_int64_new(ns_get16(cp + 4)));
			ucv_array_push(item, ucv_string_new(dname));
			ucv_array_push(type_arr, item);
			break;

		case ns_t_soa:
			if (rdlen < 20) {
				set_error(EBADMSG, "SOA record too short");

				return -1;
			}

			type_arr = init_obj(vm, name_obj, "SOA", UC_ARRAY);
			item = ucv_array_new_length(vm, 7);

			cp = ns_rr_rdata(rr);
			n = ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
			                       cp, dname, sizeof(dname));

			if (n < 0) {
				set_error(errno, "Unable to uncompress domain name");
				ucv_put(item);

				return -1;
			}

			ucv_array_push(item, ucv_string_new(dname)); /* origin */
			cp += n;

			n = ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
			                       cp, dname, sizeof(dname));

			if (n < 0) {
				set_error(errno, "Unable to uncompress domain name");
				ucv_put(item);

				return -1;
			}

			ucv_array_push(item, ucv_string_new(dname)); /* mail addr */
			cp += n;

			ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* serial */
			cp += 4;

			ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* refresh */
			cp += 4;

			ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* retry */
			cp += 4;

			ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* expire */
			cp += 4;

			ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* minimum */

			ucv_array_push(type_arr, item);
			break;

		default:
			break;
		}
	}

	return i;
}

static int
parse_nsaddr(const char *addrstr, addr_t *lsa)
{
	char *eptr, *hash, ifname[IFNAMSIZ], ipaddr[INET6_ADDRSTRLEN] = { 0 };
	unsigned int port = default_port;
	unsigned int scope = 0;

	hash = strchr(addrstr, '#');

	if (hash) {
		port = strtoul(hash + 1, &eptr, 10);

		if ((size_t)(hash - addrstr) >= sizeof(ipaddr) ||
		    eptr == hash + 1 || *eptr != '\0' || port > 65535) {
			errno = EINVAL;
			return -1;
		}

		memcpy(ipaddr, addrstr, hash - addrstr);
	}
	else {
		strncpy(ipaddr, addrstr, sizeof(ipaddr) - 1);
	}

	hash = strchr(addrstr, '%');

	if (hash) {
		for (eptr = ++hash; *eptr != '\0' && *eptr != '#'; eptr++) {
			if ((eptr - hash) >= IFNAMSIZ) {
				errno = ENODEV;
				return -1;
			}

			ifname[eptr - hash] = *eptr;
		}

		ifname[eptr - hash] = '\0';
		scope = if_nametoindex(ifname);

		if (scope == 0) {
			errno = ENODEV;
			return -1;
		}
	}

	if (inet_pton(AF_INET6, ipaddr, &lsa->u.sin6.sin6_addr)) {
		lsa->u.sin6.sin6_family = AF_INET6;
		lsa->u.sin6.sin6_port = htons(port);
		lsa->u.sin6.sin6_scope_id = scope;
		lsa->len = sizeof(lsa->u.sin6);
		return 0;
	}

	if (!scope && inet_pton(AF_INET, ipaddr, &lsa->u.sin.sin_addr)) {
		lsa->u.sin.sin_family = AF_INET;
		lsa->u.sin.sin_port = htons(port);
		lsa->len = sizeof(lsa->u.sin);
		return 0;
	}

	errno = EINVAL;
	return -1;
}

static char *
make_ptr(const char *addrstr)
{
	const char *hexdigit = "0123456789abcdef";
	static char ptrstr[73];
	unsigned char addr[16];
	char *ptr = ptrstr;
	int i;

	if (inet_pton(AF_INET6, addrstr, addr)) {
		if (memcmp(addr, "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12) != 0) {
			for (i = 0; i < 16; i++) {
				*ptr++ = hexdigit[(unsigned char)addr[15 - i] & 0xf];
				*ptr++ = '.';
				*ptr++ = hexdigit[(unsigned char)addr[15 - i] >> 4];
				*ptr++ = '.';
			}
			strcpy(ptr, "ip6.arpa");
		}
		else {
			sprintf(ptr, "%u.%u.%u.%u.in-addr.arpa",
			        addr[15], addr[14], addr[13], addr[12]);
		}

		return ptrstr;
	}

	if (inet_pton(AF_INET, addrstr, addr)) {
		sprintf(ptr, "%u.%u.%u.%u.in-addr.arpa",
		        addr[3], addr[2], addr[1], addr[0]);
		return ptrstr;
	}

	return NULL;
}

static unsigned long
mtime(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_REALTIME, &ts);

	return (unsigned long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

static void
to_v4_mapped(addr_t *a)
{
	if (a->u.sa.sa_family != AF_INET)
		return;

	memcpy(a->u.sin6.sin6_addr.s6_addr + 12,
	       &a->u.sin.sin_addr, 4);

	memcpy(a->u.sin6.sin6_addr.s6_addr,
	       "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12);

	a->u.sin6.sin6_family = AF_INET6;
	a->u.sin6.sin6_flowinfo = 0;
	a->u.sin6.sin6_scope_id = 0;
	a->len = sizeof(a->u.sin6);
}

static void
add_status(uc_vm_t *vm, uc_value_t *res_obj, const char *name, const char *rcode)
{
	uc_value_t *name_obj = init_obj(vm, res_obj, name, UC_OBJECT);

	ucv_object_add(name_obj, "rcode", ucv_string_new(rcode));
}

/*
 * Function logic borrowed & modified from musl libc, res_msend.c
 */

static int
send_queries(resolve_ctx_t *ctx, uc_vm_t *vm, uc_value_t *res_obj)
{
	int fd, flags;
	int servfail_retry = 0;
	addr_t from = { };
	int one = 1;
	int recvlen = 0;
	int n_replies = 0;
	struct pollfd pfd;
	unsigned long t0, t1, t2, timeout = ctx->timeout, retry_interval;
	unsigned int nn, qn, next_query = 0;
	struct { unsigned char *buf; size_t len; } reply_buf = { 0 };

	from.u.sa.sa_family = AF_INET;
	from.len = sizeof(from.u.sin);

	for (nn = 0; nn < ctx->n_ns; nn++) {
		if (ctx->ns[nn].addr.u.sa.sa_family == AF_INET6) {
			from.u.sa.sa_family = AF_INET6;
			from.len = sizeof(from.u.sin6);
			break;
		}
	}

#ifdef __APPLE__
	flags = SOCK_DGRAM;
#else
	flags = SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK;
#endif

	/* Get local address and open/bind a socket */
	fd = socket(from.u.sa.sa_family, flags, 0);

	/* Handle case where system lacks IPv6 support */
	if (fd < 0 && from.u.sa.sa_family == AF_INET6 && errno == EAFNOSUPPORT) {
		fd = socket(AF_INET, flags, 0);
		from.u.sa.sa_family = AF_INET;
	}

	if (fd < 0) {
		set_error(errno, "Unable to open UDP socket");

		return -1;
	}

#ifdef __APPLE__
	flags = fcntl(fd, F_GETFD);

	if (flags < 0) {
		set_error(errno, "Unable to acquire socket descriptor flags");
		close(fd);

		return -1;
	}

	if (fcntl(fd, F_SETFD, flags|O_CLOEXEC|O_NONBLOCK) < 0) {
		set_error(errno, "Unable to set socket descriptor flags");
		close(fd);

		return -1;
	}
#endif

	if (bind(fd, &from.u.sa, from.len) < 0) {
		set_error(errno, "Unable to bind UDP socket");
		close(fd);

		return -1;
	}

	/* Convert any IPv4 addresses in a mixed environment to v4-mapped */
	if (from.u.sa.sa_family == AF_INET6) {
		setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one));

		for (nn = 0; nn < ctx->n_ns; nn++)
			to_v4_mapped(&ctx->ns[nn].addr);
	}

	pfd.fd = fd;
	pfd.events = POLLIN;
	retry_interval = timeout / ctx->retries;
	t0 = t2 = mtime();
	t1 = t2 - retry_interval;

	for (; t2 - t0 < timeout; t2 = mtime()) {
		if (t2 - t1 >= retry_interval) {
			for (qn = 0; qn < ctx->n_queries; qn++) {
				if (ctx->queries[qn].rcode == 0 || ctx->queries[qn].rcode == 3)
					continue;

				for (nn = 0; nn < ctx->n_ns; nn++) {
					sendto(fd, ctx->queries[qn].query, ctx->queries[qn].qlen,
					       MSG_NOSIGNAL, &ctx->ns[nn].addr.u.sa, ctx->ns[nn].addr.len);
				}
			}

			t1 = t2;
			servfail_retry = 2 * ctx->n_queries;
		}

		/* Wait for a response, or until time to retry */
		switch (poll(&pfd, 1, t1+retry_interval-t2)) {
		case 0:
			/* timeout */
			for (qn = 0; qn < ctx->n_queries; qn++) {
				if (ctx->queries[qn].rcode != -1)
					continue;

				for (nn = 0; nn < ctx->n_ns; nn++)
					add_status(vm, res_obj, ctx->queries[qn].name, "TIMEOUT");
			}

			continue;

		case -1:
			/* error */
			continue;
		}

		while (1) {
			recvlen = recvfrom(fd, NULL, 0, MSG_PEEK|MSG_TRUNC, &from.u.sa, &from.len);

			/* read error */
			if (recvlen < 0)
				break;

			if ((size_t)recvlen > reply_buf.len) {
				reply_buf.buf = xrealloc(reply_buf.buf, recvlen);
				reply_buf.len = recvlen;
			}

			recvlen = recvfrom(fd, reply_buf.buf, recvlen, 0, &from.u.sa, &from.len);

			/* Ignore non-identifiable packets */
			if (recvlen < 4)
				continue;

			/* Ignore replies from addresses we didn't send to */
			for (nn = 0; nn < ctx->n_ns; nn++)
				if (memcmp(&from.u.sa, &ctx->ns[nn].addr.u.sa, from.len) == 0)
					break;

			if (nn >= ctx->n_ns)
				continue;

			/* Find which query this answer goes with, if any */
			for (qn = next_query; qn < ctx->n_queries; qn++)
				if (!memcmp(reply_buf.buf, ctx->queries[qn].query, 2))
					break;

			/* Do not overwrite previous replies from other servers
			 * but allow overwriting preexisting NXDOMAIN reply */
			if (qn >= ctx->n_queries ||
			    ctx->queries[qn].rcode == 0 ||
			    (ctx->queries[qn].rcode == 3 && (reply_buf.buf[3] & 15) != 0))
				continue;

			ctx->queries[qn].rcode = reply_buf.buf[3] & 15;

			switch (ctx->queries[qn].rcode) {
			case 0:
				ucv_object_delete(
					ucv_object_get(res_obj, ctx->queries[qn].name, NULL),
					"rcodes");

				break;

			case 2:
				/* Retry immediately on server failure. */
				if (servfail_retry && servfail_retry--)
					sendto(fd, ctx->queries[qn].query, ctx->queries[qn].qlen,
					       MSG_NOSIGNAL, &ctx->ns[nn].addr.u.sa, ctx->ns[nn].addr.len);

				/* fall through */

			default:
				add_status(vm, res_obj, ctx->queries[qn].name,
				           rcodes[ctx->queries[qn].rcode]);
			}

			/* Store answer */
			n_replies++;

			ctx->queries[qn].rlen = recvlen;

			parse_reply(vm, res_obj, reply_buf.buf, recvlen);

			if (qn == next_query) {
				while (next_query < ctx->n_queries) {
					if (ctx->queries[next_query].rcode == -1)
						break;

					next_query++;
				}
			}

			if (next_query >= ctx->n_queries)
				goto out;
		}
	}

out:
	free(reply_buf.buf);
	close(fd);

	return n_replies;
}

static ns_t *
add_ns(resolve_ctx_t *ctx, const char *addr)
{
	char portstr[sizeof("65535")], *p;
	addr_t a = { };
	struct addrinfo *ai, *aip, hints = {
		.ai_flags = AI_NUMERICSERV,
		.ai_socktype = SOCK_DGRAM
	};

	if (parse_nsaddr(addr, &a)) {
		/* Maybe we got a domain name, attempt to resolve it using the standard
		 * resolver routines */

		p = strchr(addr, '#');
		snprintf(portstr, sizeof(portstr), "%hu",
		         (unsigned short)(p ? strtoul(p, NULL, 10) : default_port));

		if (!getaddrinfo(addr, portstr, &hints, &ai)) {
			for (aip = ai; aip; aip = aip->ai_next) {
				if (aip->ai_addr->sa_family != AF_INET &&
				    aip->ai_addr->sa_family != AF_INET6)
					continue;

				ctx->ns = xrealloc(ctx->ns, sizeof(*ctx->ns) * (ctx->n_ns + 1));
				ctx->ns[ctx->n_ns].name = addr;
				ctx->ns[ctx->n_ns].addr.len = aip->ai_addrlen;

				memcpy(&ctx->ns[ctx->n_ns].addr.u.sa, aip->ai_addr, aip->ai_addrlen);

				ctx->n_ns++;
			}

			freeaddrinfo(ai);

			return &ctx->ns[ctx->n_ns];
		}

		return NULL;
	}

	ctx->ns = xrealloc(ctx->ns, sizeof(*ctx->ns) * (ctx->n_ns + 1));
	ctx->ns[ctx->n_ns].addr = a;
	ctx->ns[ctx->n_ns].name = addr;

	return &ctx->ns[ctx->n_ns++];
}

static int
parse_resolvconf(resolve_ctx_t *ctx)
{
	int prev_n_ns = ctx->n_ns;
	char line[128], *p;
	FILE *resolv;
	bool ok;

	if ((resolv = fopen("/etc/resolv.conf", "r")) != NULL) {
		while (fgets(line, sizeof(line), resolv)) {
			p = strtok(line, " \t\n");

			if (!p || strcmp(p, "nameserver"))
				continue;

			p = strtok(NULL, " \t\n");

			if (!p)
				continue;

			p = xstrdup(p);
			ok = add_ns(ctx, p);

			free(p);

			if (!ok)
				break;
		}

		fclose(resolv);
	}

	return ctx->n_ns - prev_n_ns;
}

static query_t *
add_query(resolve_ctx_t *ctx, int type, const char *dname)
{
	opt_rr_t *opt;
	ssize_t qlen;

	ctx->queries = xrealloc(ctx->queries, sizeof(*ctx->queries) * (ctx->n_queries + 1));

	memset(&ctx->queries[ctx->n_queries], 0, sizeof(*ctx->queries));

	qlen = res_mkquery(QUERY, dname, C_IN, type, NULL, 0, NULL,
	                   ctx->queries[ctx->n_queries].query,
	                   sizeof(ctx->queries[ctx->n_queries].query));

	/* add OPT record */
	if (ctx->edns_maxsize != 0 && qlen + sizeof(opt_rr_t) <= sizeof(ctx->queries[ctx->n_queries].query)) {
		ctx->queries[ctx->n_queries].query[11] = 1;

		opt = (opt_rr_t *)&ctx->queries[ctx->n_queries].query[qlen];
		opt->root_domain = 0;
		opt->type = htons(41);
		opt->edns_maxsize = htons(ctx->edns_maxsize);
		opt->extended_rcode = 0;
		opt->edns_version = 0;
		opt->z = htons(0);
		opt->data_length = htons(0);

		qlen += sizeof(opt_rr_t);
	}

	ctx->queries[ctx->n_queries].qlen = qlen;
	ctx->queries[ctx->n_queries].name = xstrdup(dname);
	ctx->queries[ctx->n_queries].rcode = -1;

	return &ctx->queries[ctx->n_queries++];
}

static bool
check_types(uc_value_t *typenames, uint32_t *types)
{
	size_t i;

	*types = 0;

	for_each_item(typenames, typename) {
		if (ucv_type(typename) != UC_STRING)
			err_return(EINVAL, "Query type value not a string");

		for (i = 0; qtypes[i].name; i++) {
			if (!strcasecmp(ucv_string_get(typename), qtypes[i].name)) {
				*types |= (1 << i);
				break;
			}
		}

		if (!qtypes[i].name)
			err_return(EINVAL, "Unrecognized query type '%s'",
			           ucv_string_get(typename));
	}

	return true;
}

static void
add_queries(resolve_ctx_t *ctx, uc_value_t *name)
{
	char *s = ucv_string_get(name);
	char *ptr;
	size_t i;

	if (ctx->qtypes == 0) {
		ptr = make_ptr(s);

		if (ptr) {
			add_query(ctx, ns_t_ptr, ptr);
		}
		else {
			add_query(ctx, ns_t_a, s);
			add_query(ctx, ns_t_aaaa, s);
		}
	}
	else {
		for (i = 0; qtypes[i].name; i++) {
			if (ctx->qtypes & (1 << i)) {
				if (qtypes[i].type == ns_t_ptr) {
					ptr = make_ptr(s);
					add_query(ctx, ns_t_ptr, ptr ? ptr : s);
				}
				else {
					add_query(ctx, qtypes[i].type, s);
				}
			}
		}
	}
}

static bool
parse_options(resolve_ctx_t *ctx, uc_value_t *opts)
{
	uc_value_t *v;

	if (!check_types(ucv_object_get(opts, "type", NULL), &ctx->qtypes))
		return false;

	for_each_item(ucv_object_get(opts, "nameserver", NULL), server) {
		if (ucv_type(server) != UC_STRING)
			err_return(EINVAL, "Nameserver value not a string");

		if (!add_ns(ctx, ucv_string_get(server)))
			err_return(EINVAL, "Unable to resolve nameserver address '%s'",
			           ucv_string_get(server));
	}

	/* Find NS servers in resolv.conf if none provided */
	if (ctx->n_ns == 0)
		parse_resolvconf(ctx);

	/* Fall back to localhost if we could not find NS in resolv.conf */
	if (ctx->n_ns == 0)
		add_ns(ctx, "127.0.0.1");

	v = ucv_object_get(opts, "retries", NULL);

	if (ucv_type(v) == UC_INTEGER)
		ctx->retries = ucv_uint64_get(v);
	else if (v)
		err_return(EINVAL, "Retries value not an integer");

	v = ucv_object_get(opts, "timeout", NULL);

	if (ucv_type(v) == UC_INTEGER)
		ctx->timeout = ucv_uint64_get(v);
	else if (v)
		err_return(EINVAL, "Timeout value not an integer");

	v = ucv_object_get(opts, "edns_maxsize", NULL);

	if (ucv_type(v) == UC_INTEGER)
		ctx->edns_maxsize = ucv_uint64_get(v);
	else if (v)
		err_return(EINVAL, "EDNS max size not an integer");

	return true;
}

/**
 * Perform DNS queries for specified domain names.
 *
 * The `query()` function performs DNS lookups for one or more domain names
 * according to the specified options. It returns a structured object containing
 * all resolved DNS records grouped by domain name and record type.
 *
 * If no record types are specified in the options, the function will perform
 * both A and AAAA record lookups for regular domain names, or PTR record
 * lookups for IP addresses (reverse DNS).
 *
 * Returns an object containing DNS query results organized by domain name.
 *
 * Raises a runtime exception if invalid arguments are provided or if DNS
 * resolution encounters critical errors.
 *
 * @function module:resolv#query
 *
 * @param {string|string[]} names
 * Domain name(s) to query. Can be a single domain name string or an array
 * of domain name strings. IP addresses can also be provided for reverse
 * DNS lookups.
 *
 * @param {object} [options]
 * Query options object.
 *
 * @param {string[]} [options.type]
 * Array of DNS record types to query for. Valid types are: 'A', 'AAAA',
 * 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT', 'ANY'. If not specified,
 * defaults to 'A' and 'AAAA' for domain names, or 'PTR' for IP addresses.
 *
 * @param {string[]} [options.nameserver]
 * Array of DNS nameserver addresses to query. Each address can optionally
 * include a port number using '#' separator (e.g., '8.8.8.8#53'). IPv6
 * addresses can include interface scope using '%' separator. If not specified,
 * nameservers are read from /etc/resolv.conf, falling back to '127.0.0.1'.
 *
 * @param {number} [options.timeout=5000]
 * Total timeout for all queries in milliseconds.
 *
 * @param {number} [options.retries=2]
 * Number of retry attempts for failed queries.
 *
 * @param {number} [options.edns_maxsize=4096]
 * Maximum UDP packet size for EDNS (Extension Mechanisms for DNS). Set to 0
 * to disable EDNS.
 *
 * @returns {object}
 * Object containing DNS query results. Keys are domain names, values are
 * objects containing arrays of records grouped by type, or error information
 * for failed queries.
 *
 * @example
 * // Basic A and AAAA record lookup
 * const result = query('example.com');
 * print(result, "\n");
 * // {
 * //   "example.com": {
 * //     "A": ["192.0.2.1"],
 * //     "AAAA": ["2001:db8::1"]
 * //   }
 * // }
 *
 * @example
 * // Specific record type queries
 * const mxResult = query('example.com', { type: ['MX'] });
 * print(mxResult, "\n");
 * // {
 * //   "example.com": {
 * //     "MX": [[10, "mail.example.com"]]
 * //   }
 * // }
 *
 * @example
 * // Multiple domains and types with custom nameserver
 * const results = query(
 *   ['example.com', 'google.com'],
 *   {
 *     type: ['A', 'MX'],
 *     nameserver: ['8.8.8.8', '1.1.1.1'],
 *     timeout: 10000
 *   }
 * );
 *
 * @example
 * // Reverse DNS lookup
 * const ptrResult = query(['192.0.2.1'], { type: ['PTR'] });
 * print(ptrResult, "\n");
 * // {
 * //   "1.2.0.192.in-addr.arpa": {
 * //     "PTR": ["example.com"]
 * //   }
 * // }
 *
 * @example
 * // Handling errors
 * const errorResult = query(['nonexistent.example.com']);
 * print(errorResult, "\n");
 * // {
 * //   "nonexistent.example.com": {
 * //     "rcode": "NXDOMAIN"
 * //   }
 * // }
 */
static uc_value_t *
uc_resolv_query(uc_vm_t *vm, size_t nargs)
{
	resolve_ctx_t ctx = { .retries = 2, .timeout = 5000, .edns_maxsize = 4096 };
	uc_value_t *names = uc_fn_arg(0);
	uc_value_t *opts = uc_fn_arg(1);
	uc_value_t *res_obj = NULL;

	if (!parse_options(&ctx, opts))
		goto err;

	for_each_item(names, name) {
		if (ucv_type(name) != UC_STRING) {
			set_error(EINVAL, "Domain name value not a string");
			goto err;
		}

		add_queries(&ctx, name);
	}

	res_obj = ucv_object_new(vm);

	if (send_queries(&ctx, vm, res_obj) == 0)
		set_error(ETIMEDOUT, "Server did not respond");

err:
	while (ctx.n_queries)
		free(ctx.queries[--ctx.n_queries].name);

	free(ctx.queries);
	free(ctx.ns);

	return res_obj;
}

/**
 * Get the last error message from DNS operations.
 *
 * The `error()` function returns a descriptive error message for the last
 * failed DNS operation, or `null` if no error occurred. This function is
 * particularly useful for debugging DNS resolution issues.
 *
 * After calling this function, the stored error state is cleared, so
 * subsequent calls will return `null` unless a new error occurs.
 *
 * Returns a string describing the last error, or `null` if no error occurred.
 *
 * @function module:resolv#error
 *
 * @returns {string|null}
 * A descriptive error message for the last failed operation, or `null` if
 * no error occurred.
 *
 * @example
 * // Check for errors after a failed query
 * const result = query("example.org", { nameserver: "invalid..domain" });
 * const err = error();
 * if (err) {
 *   print("DNS query failed: ", err, "\n");
 * }
 */
static uc_value_t *
uc_resolv_error(uc_vm_t *vm, size_t nargs)
{
	uc_stringbuf_t *buf;
	const char *s;

	if (last_error.code == 0)
		return NULL;

	buf = ucv_stringbuf_new();

	s = strerror(last_error.code);

	ucv_stringbuf_addstr(buf, s, strlen(s));

	if (last_error.msg)
		ucv_stringbuf_printf(buf, ": %s", last_error.msg);

	set_error(0, NULL);

	return ucv_stringbuf_finish(buf);
}


static const uc_function_list_t resolv_fns[] = {
	{ "query",	uc_resolv_query },
	{ "error",	uc_resolv_error },
};

void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
{
	uc_function_list_register(scope, resolv_fns);
}