public inbox for gwml@vger.gnuweeb.org
 help / color / mirror / Atom feed
From: Ahmad Gani <reyuki@gnuweeb.org>
To: Ammar Faizi <ammarfaizi2@gnuweeb.org>
Cc: Ahmad Gani <reyuki@gnuweeb.org>,
	Alviro Iskandar Setiawan <alviro.iskandar@gnuweeb.org>,
	GNU/Weeb Mailing List <gwml@vger.gnuweeb.org>
Subject: [PATCH gwproxy v1 3/3] dnslookup: Initial work for implementation of C-ares-like getaddrinfo function
Date: Thu, 31 Jul 2025 10:07:46 +0700	[thread overview]
Message-ID: <20250731030856.366368-4-reyuki@gnuweeb.org> (raw)
In-Reply-To: <20250731030856.366368-1-reyuki@gnuweeb.org>

Introducing glibc's getaddrinfo replacement, the DNS protocol
implementation is limited to standard query (OPCODE_QUERY) as for now,
but may extended later as necessary.

Signed-off-by: Ahmad Gani <reyuki@gnuweeb.org>
---
 src/gwproxy/dnslookup.c | 286 ++++++++++++++++++++++++++++++++
 src/gwproxy/dnslookup.h | 118 +++++++++++++
 src/gwproxy/dnsparser.c | 357 ++++++++++++++++++++++++++++++++++++++++
 src/gwproxy/dnsparser.h | 191 +++++++++++++++++++++
 4 files changed, 952 insertions(+)
 create mode 100644 src/gwproxy/dnslookup.c
 create mode 100644 src/gwproxy/dnslookup.h
 create mode 100644 src/gwproxy/dnsparser.c
 create mode 100644 src/gwproxy/dnsparser.h

diff --git a/src/gwproxy/dnslookup.c b/src/gwproxy/dnslookup.c
new file mode 100644
index 000000000000..5b87e7248d54
--- /dev/null
+++ b/src/gwproxy/dnslookup.c
@@ -0,0 +1,286 @@
+/*
+ * DNS lookup: an implementation of DNS resolution for IPv4 and IPv6.
+ */
+
+#include <gwproxy/net.h>
+#include <gwproxy/dnslookup.h>
+#include <gwproxy/dnsparser.h>
+#include <gwproxy/syscall.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+static int resolve_name(int sockfd, const char *name, uint16_t type, uint16_t nport,
+	struct gw_ares_addrinfo *result, struct gw_addrinfo_node **tail)
+{
+	uint8_t send_buff[UDP_MSG_LIMIT];
+	uint8_t recv_buff[UDP_MSG_LIMIT];
+	gwdns_query_pkt *query_pkt;
+	gwdns_answ_data raw_answ;
+	gwdns_question_part q;
+	ssize_t buff_len;
+	int ret;
+
+	q.domain = name;
+	q.type = type;
+	q.dst_buffer = send_buff;
+	q.dst_len = UDP_MSG_LIMIT;
+	buff_len = construct_question(&q);
+	if (buff_len < 0) {
+		/*
+		 * I'm confident that 512 bytes is more than sufficient
+		 * to construct single dns query packet.
+		 */
+		assert(buff_len != -ENOBUFS);
+		ret = GW_ARES_EINVAL;
+		return ret;
+	}
+	query_pkt = (void *)send_buff;
+
+attempt_retry:
+	ret = __sys_send(sockfd, send_buff, buff_len, MSG_NOSIGNAL);
+	if (ret < 0) {
+		ret = GW_ARES_INTERNAL_ERR;
+		return ret;
+	}
+
+	if (ret != buff_len)
+		goto attempt_retry;
+
+	ret = __sys_recv(sockfd, recv_buff, UDP_MSG_LIMIT, MSG_NOSIGNAL);
+	if (ret < 0) {
+		ret = GW_ARES_INTERNAL_ERR;
+		return ret;
+	}
+
+	/* 
+	 * TODO(reyuki): even though it's unlikely,
+	 * but what todo when the connection is closed?
+	 * drop the request or retry?
+	 */
+	assert(ret);
+
+	ret = serialize_answ(query_pkt->hdr.id, recv_buff, ret, &raw_answ);
+	if (ret) {
+		/*
+		 * TODO(reyuki): the reason of failure can vary,
+		 * but what to do in that case? EAGAIN could possibly indicate
+		 * short recv, but this is UDP packet, retry from send?
+		 * or just drop the request?
+		 */
+	}
+	assert(!ret);
+
+	/* TODO(reyuki): hints->ai_family is used to filter results */
+	for (size_t i = 0; i < raw_answ.hdr.ancount; i++) {
+		gwdns_serialized_answ *answ = raw_answ.rr_answ[i];
+		struct gw_addrinfo_node *new_node = malloc(sizeof(*new_node));
+		if (!new_node) {
+			ret = GW_ARES_ENOMEM;
+			goto exit_free;
+		}
+		new_node->ai_next = NULL;
+
+		if (answ->rr_type == TYPE_AAAA) {
+			new_node->ai_family = AF_INET6;
+			new_node->ai_addrlen = sizeof(new_node->ai_addr.i6);
+			new_node->ai_addr.i6.sin6_port = nport;
+			new_node->ai_addr.i6.sin6_family = AF_INET6;
+			/*
+			 * no overflow.
+			 * it's guaranteed to be true by serialize_answ function
+			 */
+			assert(sizeof(new_node->ai_addr.i6.sin6_addr) == answ->rdlength);
+			memcpy(&new_node->ai_addr.i6.sin6_addr, answ->rdata, answ->rdlength);
+		} else {
+			new_node->ai_family = AF_INET;
+			new_node->ai_addrlen = sizeof(new_node->ai_addr.i4);
+			new_node->ai_addr.i4.sin_port = nport;
+			new_node->ai_addr.i4.sin_family = AF_INET;
+			/*
+			 * no overflow.
+			 * it's guaranteed to be true by serialize_answ function
+			 */
+			assert(sizeof(new_node->ai_addr.i4.sin_addr) == answ->rdlength);
+			memcpy(&new_node->ai_addr.i4.sin_addr, answ->rdata, answ->rdlength);
+			new_node->ai_ttl = answ->ttl;
+		}
+
+		if (!*tail)
+			result->nodes = new_node;
+		else
+			(*tail)->ai_next = new_node;
+		*tail = new_node;
+	}
+
+	ret = GW_ARES_SUCCESS;
+exit_free:
+	free_serialize_answ(&raw_answ);
+	return ret;
+}
+
+void gw_ares_getaddrinfo(gw_ares_channel_t *channel,
+			const char *name, const char *service,
+			const struct gw_ares_addrinfo_hints *hints,
+			gw_ares_addrinfo_callback callback, void *arg)
+{
+	struct gw_ares_addrinfo *result;
+	struct gw_addrinfo_node *tail;
+	struct gwp_sockaddr *addr;
+	socklen_t addrlen;
+	int ret, sockfd;
+	uint16_t nport;
+	uint8_t mask;
+
+	switch (hints->ai_family) {
+		case AF_UNSPEC:
+			mask = I6_BIT | I4_BIT;
+			break;
+		case AF_INET:
+			mask = I4_BIT;
+			break;
+		case AF_INET6:
+			mask = I6_BIT;
+			break;
+		default:
+			ret = GW_ARES_EINVAL;
+			goto error;
+	}
+
+	nport = (uint16_t)atoi(service);
+	if (!nport) {
+		ret = GW_ARES_EINVAL;
+		goto error;
+	}
+	nport = htons(nport);
+
+	result = malloc(sizeof(*result));
+	if (!result) {
+		ret = GW_ARES_ENOMEM;
+		goto error;
+	}
+
+	addr = &channel->servers[0];
+	sockfd = __sys_socket(addr->sa.sa_family, SOCK_DGRAM, 0);
+	if (sockfd < 0) {
+		ret = GW_ARES_INTERNAL_ERR;
+		goto error_free;
+	}
+
+	addrlen = addr->sa.sa_family == AF_INET ? sizeof(addr->i4) : sizeof(addr->i6);
+	ret = __sys_connect(sockfd, &addr->sa, addrlen);
+	if (ret < 0) {
+		ret = GW_ARES_INTERNAL_ERR;
+		goto error_close;
+	}
+
+	tail = NULL;
+	if (IS_I6(mask)) {
+		ret = resolve_name(sockfd, name, TYPE_AAAA, nport, result, &tail);
+		if (ret)
+			goto error_close;
+	}
+
+	if (IS_I4(mask)) {
+		ret = resolve_name(sockfd, name, TYPE_A, nport, result, &tail);
+		if (ret)
+			goto error_close;
+	}
+
+	callback(arg, GW_ARES_SUCCESS, result);
+	return;
+error_close:
+	__sys_close(sockfd);
+error_free:
+	free(result);
+error:
+	callback(arg, ret, result);
+}
+
+void gw_ares_freeaddrinfo(struct gw_ares_addrinfo *ai)
+{
+	struct gw_addrinfo_node *tmp, *node = ai->nodes;
+	while (node) {
+		tmp = node->ai_next;
+		free(node);
+		node = tmp;
+	}
+
+	free(ai);
+}
+
+int gw_ares_init(gw_ares_channel_t **channel, struct gw_ares_options *opts)
+{
+	gw_ares_channel_t *c;
+	int ret;
+
+	if (!opts->nr_server)
+		return -EINVAL;
+
+	*channel = malloc(sizeof(**channel));
+	if (!*channel)
+		return -ENOMEM;
+
+	c = *channel;
+	c->nr_server = opts->nr_server;
+	c->servers = malloc(c->nr_server * sizeof(*c->servers));
+	if (!c->servers) {
+		free(*channel);
+		return -ENOMEM;
+	}
+	/*
+	 * TODO(reyuki): validate flags and
+	 * for now use it to control recursion desired (RD) bit?
+	 */
+	c->flags = opts->flags;
+	for (int i = 0; i < c->nr_server; i++) {
+		ret = init_addr(opts->servers[i], &c->servers[i], DEFAULT_DOMAIN_PORT);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+void gw_ares_deinit(gw_ares_channel_t *channel)
+{
+	free(channel->servers);
+	free(channel);
+}
+
+static void gw_ares_cb(void *arg, int status, struct gw_ares_addrinfo *result)
+{
+	struct gw_addrinfo_node *node;
+	char buf[FULL_ADDRSTRLEN];
+
+	(void)arg;
+
+	assert(!status);
+	node = result->nodes;
+	while (node) {
+		int r = convert_ssaddr_to_str(buf, &node->ai_addr);
+		assert(!r);
+		printf("%s: %s\n", node->ai_family == AF_INET6 ? "IPv6" : "IPv4", buf);
+		node = node->ai_next;
+	}
+	gw_ares_freeaddrinfo(result);
+}
+
+int main(void)
+{
+	gw_ares_channel_t *channel;
+	struct gw_ares_addrinfo_hints hints = {
+		.ai_family = AF_UNSPEC
+	};
+	const char *servers[] = {"1.1.1.1", "8.8.8.8"};
+	struct gw_ares_options opts = {
+		.flags = 0,
+		.nr_server = 1,
+		.servers = servers
+	};
+	int ret;
+	ret = gw_ares_init(&channel, &opts);
+	if (ret)
+		return -EXIT_FAILURE;
+	gw_ares_getaddrinfo(channel, "google.com", "80", &hints, gw_ares_cb, NULL);
+	gw_ares_deinit(channel);
+}
diff --git a/src/gwproxy/dnslookup.h b/src/gwproxy/dnslookup.h
new file mode 100644
index 000000000000..a4ed02cab5f1
--- /dev/null
+++ b/src/gwproxy/dnslookup.h
@@ -0,0 +1,118 @@
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <gwproxy/net.h>
+
+#define DEFAULT_DOMAIN_PORT 53
+#define I6_BIT (1u << 0)
+#define I4_BIT (1u << 1)
+#define IS_I6(X) (((X) & I6_BIT) != 0)
+#define IS_I4(X) (((X) & I4_BIT) != 0)
+
+enum {
+	GW_ARES_SUCCESS		= 0,
+	GW_ARES_ENOMEM		= 1,
+	GW_ARES_EINVAL		= 2,
+
+	/*
+	 * internal error can be interpreted as system call failure, 
+	 * however, the cause of it can be vary.
+	 */
+	GW_ARES_INTERNAL_ERR	= 3
+};
+
+struct gw_ares_options {
+	int		flags;
+	int		nr_server;
+
+	/* 
+	 * the string format is ip:port, the ip may be encapsulated with square
+	 * brackets ([]), and must be if using IPv6.
+	 */
+	const char	**servers;
+};
+
+struct gw_ares_channeldata {
+	int			flags;
+	int			nr_server;
+	/* currently only index 0 is used, and others are ignored. */
+	struct gwp_sockaddr	*servers;
+};
+
+typedef struct gw_ares_channeldata gw_ares_channel_t;
+
+struct gw_ares_addrinfo_hints {
+	int ai_family;
+};
+
+/*
+ * gw_addrinfo_node structure is similar to RFC 3493 addrinfo,
+ * but without canonname and with extra ttl field.
+ * 
+ * - https://c-ares.org/docs/ares_getaddrinfo.html
+ */
+struct gw_addrinfo_node {
+	int			ai_family;
+	int			ai_ttl;
+	socklen_t		ai_addrlen;
+	struct gwp_sockaddr	ai_addr;
+	struct gw_addrinfo_node	*ai_next;
+};
+
+struct gw_ares_addrinfo {
+	struct gw_addrinfo_node *nodes;
+	char			*name;
+};
+
+/*
+ * result is only initialized if status == GW_ARES_SUCCESS.
+ */
+typedef void (*gw_ares_addrinfo_callback)(void *arg, int status,
+					struct gw_ares_addrinfo *result);
+
+/* 
+ * Initiate a host query by name and service
+ * 
+ * Description:
+ * the gw_getaddrinfo function initiate a host query
+ * by @name on the name service channel identified by @channel
+ * 
+ * @param channel
+ * @param name
+ * @param service
+ * @param hints
+ * @param callback
+ * @param arg
+ */
+void gw_ares_getaddrinfo(gw_ares_channel_t *channel,
+			const char *name, const char *service,
+			const struct gw_ares_addrinfo_hints *hints,
+			gw_ares_addrinfo_callback callback, void *arg);
+
+/*
+ * Free the resources allocated by gw_ares_getaddrinfo.
+ * 
+ * @param ai
+ */
+void gw_ares_freeaddrinfo(struct gw_ares_addrinfo *ai);
+
+/*
+ * Initialize name service communication channel.
+ * 
+ * Description:
+ * the gw_ares_init function initialize a communication channel for name
+ * service lookups.
+ * 
+ * It is recommended for an application to have at most one channel and use this
+ * for all DNS queries for the life of the application.
+ * 
+ * gw_ares_init can return any of the following values when an error occured:
+ * -EINVAL invalid options
+ * -ENOMEM insufficient memory
+ * 
+ * @param channel pointer to initialize
+ * @param opts controlling the behavior of the resolver
+ * @return zero on success and negative integer on error
+ */
+int gw_ares_init(gw_ares_channel_t **channel, struct gw_ares_options *opts);
+void gw_ares_deinit(gw_ares_channel_t *channel);
diff --git a/src/gwproxy/dnsparser.c b/src/gwproxy/dnsparser.c
new file mode 100644
index 000000000000..9e84f539ccde
--- /dev/null
+++ b/src/gwproxy/dnsparser.c
@@ -0,0 +1,357 @@
+#define _DEFAULT_SOURCE
+#include <endian.h>
+#include <gwproxy/dnsparser.h>
+
+static ssize_t construct_qname(uint8_t *dst, size_t dst_len, const char *qname)
+{
+	const uint8_t *p = (const uint8_t *)qname;
+	uint8_t *lp = dst; // Length position.
+	uint8_t *sp = lp + 1;  // String position.
+	size_t total = 0;
+	uint16_t l;
+
+	l = 0;
+	while (1) {
+		uint8_t c = *p++;
+
+		total++;
+		if (total >= dst_len)
+			return -ENAMETOOLONG;
+
+		if (c == '.' || c == '\0') {
+			if (l < 1 || l > 255)
+				return -EINVAL;
+
+			*lp = (uint8_t)l;
+			lp = sp++;
+			l = 0;
+			if (!c)
+				break;
+		} else {
+			l++;
+			*sp = c;
+			sp++;
+		}
+	}
+
+	return total;
+}
+
+static ssize_t calculate_question_len(uint8_t *in, size_t in_len)
+{
+	const uint8_t *p = in;
+	size_t tot_len, advance_len;
+
+	tot_len = 0;
+	while (true) {
+		if (*p == 0x0)
+			break;
+
+		if (tot_len >= in_len)
+			return -ENAMETOOLONG;
+
+		advance_len = *p + 1;
+		tot_len += advance_len;
+		p += advance_len;
+	}
+
+	return  tot_len;
+}
+
+int serialize_answ(uint16_t txid, uint8_t *in, size_t in_len, gwdns_answ_data *out)
+{
+	size_t advance_len, first_len;
+	gwdns_header_pkt *hdr;
+	uint16_t raw_flags;
+	int ret;
+
+	advance_len = sizeof(*hdr);
+	if (in_len < advance_len)
+		return -EAGAIN;
+
+	hdr = (void *)in;
+	if (memcmp(&txid, &hdr->id, sizeof(txid)))
+		return -EINVAL;
+
+	memcpy(&raw_flags, &in[2], sizeof(raw_flags));
+	raw_flags = ntohs(raw_flags);
+	/* QR MUST 1 = response from dns server */
+	if (!DNS_QR(raw_flags))
+		return -EINVAL;
+
+	/* OPCODE MUST 0 = standard query */
+	if (DNS_OPCODE(raw_flags))
+		return -EINVAL;
+	
+	/* RCODE MUST 0 = No error */
+	if (DNS_RCODE(raw_flags))
+		return -EPROTO;
+
+	// is it safe or recommended to alter the in buffer directly?
+	hdr->ancount = ntohs(hdr->ancount);
+	if (!hdr->ancount)
+		return -ENODATA;
+
+	in += advance_len;
+	in_len -= advance_len;
+
+	first_len = 1 + in[0];
+	advance_len = first_len + 1 + 2 + 2;
+	if (in_len < advance_len)
+		return -EAGAIN;
+
+	ret = calculate_question_len(in, in_len);
+	if (ret < 0)
+		return -EINVAL;
+
+	advance_len -= first_len;
+	advance_len += ret;
+	if (in_len < advance_len)
+		return -EAGAIN;
+
+	in += advance_len;
+	in_len -= advance_len;
+	out->hdr.ancount = 0;
+	out->rr_answ = malloc(hdr->ancount * sizeof(uint8_t *));
+	if (!out->rr_answ)
+		return -ENOMEM;
+
+	for (size_t i = 0; i < hdr->ancount; i++) {
+		uint16_t is_compressed, rdlength;
+		gwdns_serialized_answ *item = malloc(sizeof(gwdns_serialized_answ));
+		if (!item) {
+			ret = -ENOMEM;
+			goto exit_free;
+		}
+
+		out->rr_answ[i] = item;
+
+		memcpy(&is_compressed, in, sizeof(is_compressed));
+		is_compressed = DNS_IS_COMPRESSED(ntohs(is_compressed));
+		assert(is_compressed);
+		in += 2; // NAME
+
+		memcpy(&item->rr_type, in, 2);
+		item->rr_type = ntohs(item->rr_type);
+		in += 2; // TYPE
+		memcpy(&item->rr_class, in, 2);
+		item->rr_class = ntohs(item->rr_class);
+		in += 2; // CLASS
+		memcpy(&item->ttl, in, 4);
+		item->ttl = be32toh(item->ttl);
+		in += 4; // TTL
+
+		memcpy(&rdlength, in, sizeof(rdlength));
+		rdlength = ntohs(rdlength);
+		if (item->rr_type != TYPE_AAAA && item->rr_type != TYPE_A) {
+			ret = -EINVAL;
+			free(item);
+			goto exit_free;
+		}
+		if (item->rr_type == TYPE_AAAA && rdlength != sizeof(struct in6_addr)) {
+			ret = -EINVAL;
+			free(item);
+			goto exit_free;
+		}
+		if (item->rr_type == TYPE_A && rdlength != sizeof(struct in_addr)) {
+			ret = -EINVAL;
+			free(item);
+			goto exit_free;
+		}
+		item->rdlength = rdlength;
+		in += 2;
+
+		/*
+		 * considering if condition above,
+		 * maybe we don't need a malloc and just allocate fixed size
+		 * for rdata? however if this parser want to be expanded for
+		 * other dns operation (e.g OPCODE_IQUERY, etc), rdata maybe
+		 * contain more than sizeof in6_addr.
+		 */
+		item->rdata = malloc(rdlength);
+		if (!item->rdata) {
+			ret = -ENOMEM;
+			free(item);
+			goto exit_free;
+		}
+		memcpy(item->rdata, in, rdlength);
+		in += rdlength;
+		out->hdr.ancount++;
+	}
+
+	return 0;
+exit_free:
+	for (size_t i = 0; i < out->hdr.ancount; i++) {
+		free(out->rr_answ[i]->rdata);
+		free(out->rr_answ[i]);
+	}
+	free(out->rr_answ);
+	return ret;
+}
+
+void free_serialize_answ(gwdns_answ_data *answ)
+{
+	for (size_t i = 0; i < answ->hdr.ancount; i++) {
+		free(answ->rr_answ[i]->rdata);
+		free(answ->rr_answ[i]);
+	}
+	free(answ->rr_answ);
+}
+
+ssize_t construct_question(gwdns_question_part *question)
+{
+	gwdns_header_pkt *hdr;
+	gwdns_query_pkt pkt;
+	uint16_t qtype, qclass;
+	size_t required_len;
+	ssize_t bw;
+
+	if (question->type != TYPE_AAAA && question->type != TYPE_A)
+		return -EINVAL;
+
+	hdr = &pkt.hdr;
+	/*
+	* the memset implicitly set opcode to OPCODE_QUERY
+	*/
+	memset(hdr, 0, sizeof(*hdr));
+	hdr->id = htons((uint16_t)rand());
+	DNS_SET_RD(hdr->flags, true);
+	hdr->flags = htons(hdr->flags);
+	hdr->qdcount = htons(1);
+
+	/*
+	* pkt.body is interpreted as question section
+	* for layout and format, see RFC 1035 4.1.2. Question section format
+	*/
+	bw = construct_qname(pkt.body, sizeof(pkt.body) - 3, question->domain);
+	if (bw < 0)
+		return bw;
+
+	pkt.body[bw++] = 0x0;
+	qtype = htons(question->type);
+	qclass = htons(CLASS_IN);
+	memcpy(&pkt.body[bw], &qtype, 2);
+	bw += 2;
+	memcpy(&pkt.body[bw], &qclass, 2);
+	bw += 2;
+
+	required_len = sizeof(pkt.hdr) + bw;
+	if (question->dst_len < required_len)
+		return -ENOBUFS;
+
+	memcpy(question->dst_buffer, &pkt, required_len);
+
+	return required_len;
+}
+
+#ifdef RUNTEST
+
+void test_simulate_ipv4query(void)
+{
+	char buff[UDP_MSG_LIMIT];
+	gwdns_query_pkt *send_pkt;
+	uint8_t recv_pkt[] = {
+		/* Header (12 bytes) */
+		0x00, 0x00,		/* transaction ID - STUB! */
+		0x81, 0x80,		/* Flags: QR=1, AA=0, RD=1, RA=1, RCODE=0 */
+		0x00, 0x01,		/* QDCOUNT = 1 */
+		0x00, 0x06,		/* ANCOUNT = 6 */
+		0x00, 0x00,		/* NSCOUNT = 0 */
+		0x00, 0x00,		/* ARCOUNT = 0 */
+		
+		/* Question Section */
+		/* Pointer label compression may be used in answers */
+		0x06, 'g','o','o','g','l','e',
+		0x03, 'c','o','m',
+		0x00,			/* Terminate name */
+		0x00, 0x01,		/* QTYPE = A */
+		0x00, 0x01,		/* QCLASS = IN */
+
+		/* Answer Section (6 records) */
+		/* Each Answer record: name pointer, type, class, ttl, rdlength, rdata */
+		/* First Answer */
+		0xC0, 0x0C,		/* Name: pointer to offset 0x0C (start of question name) */
+		0x00, 0x01,		/* TYPE = A */
+		0x00, 0x01,		/* CLASS = IN */
+		0x00, 0x00, 0x08, 0x62, /* TTL = 0x00000862 = 2146 sec */
+		0x00, 0x04,		/* RDLENGTH = 4 */
+		0x4A, 0x7D, 0x18, 0x71,	/* RDATA = 74.125.24.113 */
+
+		/* Second Answer */
+		0xC0, 0x0C,
+		0x00, 0x01,
+		0x00, 0x01,
+		0x00, 0x00, 0x08, 0x62,
+		0x00, 0x04,
+		0x4A, 0x7D, 0x18, 0x65, /* 74.125.24.101 */
+
+		/* Third Answer */
+		0xC0, 0x0C,
+		0x00, 0x01,
+		0x00, 0x01,
+		0x00, 0x00, 0x08, 0x62,
+		0x00, 0x04,
+		0x4A, 0x7D, 0x18, 0x8B, /* 74.125.24.139 */
+
+		/* Fourth Answer */
+		0xC0, 0x0C,
+		0x00, 0x01,
+		0x00, 0x01,
+		0x00, 0x00, 0x08, 0x62,
+		0x00, 0x04,
+		0x4A, 0x7D, 0x18, 0x8A, /* 74.125.24.138 */
+
+		/* Fifth Answer */
+		0xC0, 0x0C,
+		0x00, 0x01,
+		0x00, 0x01,
+		0x00, 0x00, 0x08, 0x62,
+		0x00, 0x04,
+		0x4A, 0x7D, 0x18, 0x64, /* 74.125.24.100 */
+
+		/* Sixth Answer */
+		0xC0, 0x0C,
+		0x00, 0x01,
+		0x00, 0x01,
+		0x00, 0x00, 0x08, 0x62,
+		0x00, 0x04,
+		0x4A, 0x7D, 0x18, 0x66, /* 74.125.24.102 */
+	};
+	gwdns_answ_data d;
+	char first_label[] = "google";
+	char second_label[] = "com";
+
+	memset(&d, 0, sizeof(d));
+	gwdns_question_part q = {
+		.domain = "google.com",
+		.dst_buffer = (uint8_t *)buff,
+		.dst_len = sizeof(buff)
+	};
+	assert(construct_question(&q) > 0);
+
+	assert(buff[12] == 6);
+	assert(!memcmp(&buff[13], first_label, 6));
+
+	assert(buff[13 + 6] == 3);
+	assert(!memcmp(&buff[13 + 6 + 1], second_label, 3));
+
+	// fill the STUB
+	memcpy(recv_pkt, buff, 2);
+
+	send_pkt = (void *)buff;
+	assert(!serialize_answ(send_pkt->hdr.id, recv_pkt, sizeof(recv_pkt), &d));
+}
+
+void run_all_tests(void)
+{
+	test_simulate_ipv4query();
+	fprintf(stderr, "all tests passed!\n");
+}
+
+int main(void)
+{
+	run_all_tests();
+	return 0;
+}
+
+#endif
diff --git a/src/gwproxy/dnsparser.h b/src/gwproxy/dnsparser.h
new file mode 100644
index 000000000000..41048568240b
--- /dev/null
+++ b/src/gwproxy/dnsparser.h
@@ -0,0 +1,191 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <liburing.h>
+
+#ifndef __packed
+#define __packed __attribute__((__packed__))
+#endif
+
+/*
+ * 4. MESSAGES
+ * 4.1. Format
+ *
+ * All communications inside of the domain protocol are carried in a single
+ * format called a message. The top-level format of a message is divided
+ * into 5 sections (some of which may be empty in certain cases), shown below:
+ *
+ *     +---------------------+
+ *     |        Header       |
+ *     +---------------------+
+ *     |       Question      | the question for the name server
+ *     +---------------------+
+ *     |        Answer       | RRs answering the question
+ *     +---------------------+
+ *     |      Authority      | RRs pointing toward an authority
+ *     +---------------------+
+ *     |      Additional     | RRs holding additional information
+ *     +---------------------+
+ *
+ * These sections are defined in RFC 1035 §4.1. The Header section is always
+ * present and includes fields that specify which of the other sections follow,
+ * as well as metadata such as whether the message is a query or response,
+ * the opcode, etc.
+ */
+
+/* Flag bit position in little-endian machine */
+#define DNS_QR_BIT		0xF
+#define DNS_OPCODE_BIT		0xB	// 4-bit field
+#define DNS_AA_BIT		0xA
+#define DNS_TC_BIT		0x9
+#define DNS_RD_BIT		0x8
+#define DNS_RA_BIT		0x7
+#define DNS_Z_BIT		0x4	// 3-bit field
+#define DNS_RCODE_BIT		0x0	// 4-bit field
+#define DNS_COMPRESSION_BIT	(0x3 << 0xE)
+
+/* Flag extraction macros for listtle-endian machine */
+#define DNS_QR(flags)		(((flags) >> DNS_QR_BIT) & 0x1)
+#define DNS_OPCODE(flags)	(((flags) >> DNS_OPCODE_BIT) & 0xF)
+#define DNS_RCODE(flags)	((flags) & 0xF)
+#define DNS_IS_COMPRESSED(mask) ((mask) & DNS_COMPRESSION_BIT)
+
+/* Flag construction macros for little-endian machine */
+#define DNS_SET_RD(flags, val)	(flags) = ((flags) & ~(1 << DNS_RD_BIT)) | ((!!(val)) << DNS_RD_BIT)
+
+/* as per RFC 1035 §2.3.4. Size limits */
+#define DOMAIN_LABEL_LIMIT 63
+#define DOMAIN_NAME_LIMIT 255
+#define UDP_MSG_LIMIT 512
+
+typedef enum {
+	OPCODE_QUERY		= 0,	// Standard query (QUERY)
+	OPCODE_IQUERY		= 1,	// Inverse query (IQUERY)
+	OPCODE_STATUS		= 2,	// Server status request (STATUS)
+	OPCODE_RESERVED_MIN	= 3,	// Reserved for future use (inclusive)
+	OPCODE_RESERVED_MAX	= 15	// Reserved for future use (inclusive)
+} gwdns_op;
+
+typedef enum {
+	TYPE_A		= 1,	// IPv4 host address
+	TYPE_NS		= 2,	// an authoritative name server
+	TYPE_CNAME	= 5,	// the canonical name for an alias
+	TYPE_SOA	= 6,	// marks the start of a zone of authority
+	TYPE_MB		= 7,	// a mailbox domain name (EXPERIMENTAL)
+	TYPE_MG		= 8,	// a mail group member (EXPERIMENTAL)
+	TYPE_MR		= 9,	// a mail rename domain name (EXPERIMENTAL)
+	TYPE_NULL	= 10,	// a null RR (EXPERIMENTAL)
+	TYPE_WKS	= 11,	// a well known service description
+	TYPE_PTR	= 12,	// a domain name pointer
+	TYPE_HINFO	= 13,	// host information
+	TYPE_MINFO	= 14,	// mailbox or mail list information
+	TYPE_MX		= 15,	// mail exchange
+	TYPE_TXT	= 16,	// text strings
+	TYPE_AAAA	= 28,	// IPv6 host address
+	QTYPE_AXFR	= 252,	// A request for a transfer of an entire zone
+	QTYPE_MAILB	= 253,	// A request for mailbox-related records (MB, MG or MR)
+	QTYPE_ALL	= 255	// A request for all records
+} gwdns_type;
+
+typedef enum {
+	CLASS_IN	= 1,	// Internet
+	CLASS_CH	= 3,	// CHAOS class
+	CLASS_HS	= 4,	// Hesiod
+	QCLASS_ANY	= 255	// ANY class (matches any class)
+} gwdns_class;
+
+typedef struct {
+	uint16_t id;
+	uint16_t flags;
+	uint16_t qdcount;
+	uint16_t ancount;
+	uint16_t nscount;
+	uint16_t arcount;
+} __packed gwdns_header_pkt;
+
+typedef struct {
+	uint8_t question[UDP_MSG_LIMIT];
+	char answr[UDP_MSG_LIMIT];
+} gwdns_question_buffer;
+
+typedef struct {
+	uint8_t *dst_buffer;
+	uint16_t type;
+	size_t dst_len;
+	const char *domain;
+} gwdns_question_part;
+
+/*
+ * 4.1.3. Resource record format
+ *
+ * The answer, authority, and additional sections all share the same
+ * format: a variable number of resource records, where the number of
+ * records is specified in the corresponding count field in the header.
+ */
+typedef struct {
+	uint8_t  *name;		// DOMAIN NAME: variable‑length sequence of labels (length-byte followed by label, ending in 0), possibly compressed
+	uint16_t  rr_type;	// TYPE: two-octet code identifying the RR type (see gwdns_type)
+	uint16_t  rr_class;	// CLASS: two-octet code identifying the RR class (see gwdns_class)
+	uint32_t  ttl;		// TTL: 32-bit unsigned, time to live in seconds
+	uint16_t  rdlength;	// RDLENGTH: length in octets of RDATA
+	uint8_t  *rdata;	// RDATA: variable-length data, format depends on TYPE and CLASS
+} gwdns_serialized_rr;
+
+typedef struct {
+	char qname[DOMAIN_NAME_LIMIT];
+	uint16_t qtype;
+	uint16_t qclass;
+} gwdns_serialized_question;
+
+typedef gwdns_serialized_rr gwdns_serialized_answ;
+
+typedef struct {
+	gwdns_header_pkt hdr;
+	uint8_t body[UDP_MSG_LIMIT];
+} gwdns_query_pkt;
+
+typedef struct {
+	gwdns_header_pkt hdr;
+	gwdns_serialized_question question;
+	gwdns_serialized_answ **rr_answ;
+} gwdns_answ_data;
+
+/*
+ * Construct question packet
+ * 
+ * The caller may need to check for potential transaction ID collisions.
+ * 
+ * possible error are:
+ * - ENAMETOOLONG	domain name in question.name is too long.
+ * - ENOBUFS		length in question.dst_len is not sufficient.
+ * - EINVAL		malformed or unsupported value in question data
+ *
+ * @param	prepared question
+ * @return	length of bytes written into dst_buffer on success,
+ * 		or a negative integer on failure.
+ */
+ssize_t construct_question(gwdns_question_part *question);
+
+/*
+ * Serialize name server's answer
+ *
+ * possible error are:
+ * -EAGAIN	in buffer is not sufficient, no bytes are processed, need more data.
+ * -EINVAL	the content of in buffer is not valid.
+ * -ENOMEM	failed to allocate dynamic memory.
+ * -ENODATA	the packet didn't contain any answers.
+ * -EPROTO	the DNS server can't understand your question
+ *
+ * @param txid	transaction id of question.
+ * @param in	a pointer to buffer that want to be parsed
+ * @param out	a pointer to serialized buffer of answer to question
+ * @return	zero on success or a negative number on failure
+ */
+int serialize_answ(uint16_t txid, uint8_t *in, size_t in_len, gwdns_answ_data *out);
+void free_serialize_answ(gwdns_answ_data *answ);
-- 
Ahmad Gani


  parent reply	other threads:[~2025-07-31  3:09 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-07-31  3:07 [PATCH gwproxy v1 0/3] Initial work for DNS lookup implementation Ahmad Gani
2025-07-31  3:07 ` [PATCH gwproxy v1 1/3] dnslookup: split common functionality and struct into net.c Ahmad Gani
2025-07-31 14:01   ` Ammar Faizi
2025-07-31 18:28     ` Alviro Iskandar Setiawan
2025-07-31 18:36       ` Ammar Faizi
2025-07-31 18:42         ` Alviro Iskandar Setiawan
2025-07-31 18:53           ` Ammar Faizi
2025-07-31 19:03             ` Alviro Iskandar Setiawan
2025-07-31  3:07 ` [PATCH gwproxy v1 2/3] dnslookup: Allow only port string number Ahmad Gani
2025-07-31  3:07 ` Ahmad Gani [this message]
2025-07-31 18:19   ` [PATCH gwproxy v1 3/3] dnslookup: Initial work for implementation of C-ares-like getaddrinfo function Alviro Iskandar Setiawan
2025-07-31 19:14   ` Alviro Iskandar Setiawan
2025-08-01  1:51     ` reyuki
2025-08-01 23:32       ` Alviro Iskandar Setiawan
2025-07-31 13:39 ` [PATCH gwproxy v1 0/3] Initial work for DNS lookup implementation Ammar Faizi
2025-08-01  1:49   ` reyuki
2025-08-01  2:19     ` Ammar Faizi
2025-08-05  6:28       ` reyuki

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250731030856.366368-4-reyuki@gnuweeb.org \
    --to=reyuki@gnuweeb.org \
    --cc=alviro.iskandar@gnuweeb.org \
    --cc=ammarfaizi2@gnuweeb.org \
    --cc=gwml@vger.gnuweeb.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox