public inbox for gwml@vger.gnuweeb.org
 help / color / mirror / Atom feed
* [PATCH gwproxy v10 0/2] Initial work on integration of DNS parser lib in gwproxy
@ 2025-09-10 10:43 Ahmad Gani
  2025-09-10 10:43 ` [PATCH gwproxy v10 1/2] dnsparser: Add dns parser code Ahmad Gani
  2025-09-10 10:43 ` [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend Ahmad Gani
  0 siblings, 2 replies; 13+ messages in thread
From: Ahmad Gani @ 2025-09-10 10:43 UTC (permalink / raw)
  To: Ammar Faizi; +Cc: Ahmad Gani, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

[[ resend with realloc fixes ]]

Hi Chief,

This is revision v10 of the initial work on the integration of the DNS
parser lib in gwproxy.

There are 2 patches in this series:
- refactor gwproxy codebase (specifically, DNS system)
- add dns parser code

Please give it a test, and thanks for taking a look.

# Changelog
v1 -> v2:
- use existing convert_str_to_ssaddr instead of init_addr
- fix memory leak when init_addr (now it's replaced) failed
- modify convert_str_to_ssaddr to support default port
- bring back __cold attribute on convert_ssaddr_to_str
- don't fill a dangling pointer as Sir Alviro said
- for now it's blocking, attempt_retry label is not needed

v2 -> v3:
- remove TODO(reyuki): hints->ai_family is used to filter results
- make gw_ares_getaddrinfo asynchronous by change UDP socket to non-blocking
- move socket creation to library initialization
- make UDP socket unconnected
- restructure internal struct and program execution flow to support async
- update unit test of dns parser
- transaction id creation is delegated to caller
- fix logic bug in serialize_answ that can lead to memory error: invalid read
- rename parameter prt and split commit message
- move variable declaration in for loop according to the Sir Alviro coding-style

v3 -> v4:
- add else block in the convert_str_to_ssaddr function
- use format specifier %hu for uint16_t in printf
- update base commit to branch master from upstream remote repository

v4 -> v5:
- squash commits to eliminate changes unrelated to the commit subject
- change commit subject: use imperative form for the subject
- update base commit to branch master from upstream remote repository
- changing the email subject
- drop the c-ares style thing
- restructure dnsparser.c and dnsparser.h

v5 -> v6:
- mark this feature as experimental and disabled by default
- fix minor issues from master branch
- fix dns parser
- add dns server as gwproxy' cmdline option
- add fallback mechanism for raw dns backend

v6 -> v7:
- squash commits
- refactor gwproxy codebase (specifically, DNS system)
- add dns parser code

v7 -> v8:
- use __sys_close instead of libc's wrapper close
- add newline at EoF
- fix warning: 'af' may be used uninitialized [-Wmaybe-uninitialized]
- remove break that occured after goto statement

v8 -> v9:
- use a single UDP socket for each gwproxy worker
- expose gwp_dns_ctx members to other source files
- apply revisions from Sir Alviro's review
- apply proposals from Sir Alviro and Sir Ammar

v9 -> v10:
- fix realloc issue

Ahmad Gani (2):
  dnsparser: Add dns parser code
  gwproxy: refactor code base to add experimental raw DNS backend

 Makefile                |   2 +-
 configure               |   8 +
 src/gwproxy/common.h    |   4 +
 src/gwproxy/dns.c       | 254 ++++++++++++++---
 src/gwproxy/dns.h       |  58 +++-
 src/gwproxy/dnsparser.c | 583 ++++++++++++++++++++++++++++++++++++++++
 src/gwproxy/dnsparser.h | 192 +++++++++++++
 src/gwproxy/ev/epoll.c  | 128 ++++++++-
 src/gwproxy/gwproxy.c   | 213 ++++++++++++++-
 src/gwproxy/gwproxy.h   |  89 +++++-
 10 files changed, 1464 insertions(+), 67 deletions(-)
 create mode 100644 src/gwproxy/dnsparser.c
 create mode 100644 src/gwproxy/dnsparser.h


base-commit: b1e70e468bab135a14a8faee3ea535ace9eac211
-- 
Ahmad Gani


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH gwproxy v10 1/2] dnsparser: Add dns parser code
  2025-09-10 10:43 [PATCH gwproxy v10 0/2] Initial work on integration of DNS parser lib in gwproxy Ahmad Gani
@ 2025-09-10 10:43 ` Ahmad Gani
  2025-09-10 10:43 ` [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend Ahmad Gani
  1 sibling, 0 replies; 13+ messages in thread
From: Ahmad Gani @ 2025-09-10 10:43 UTC (permalink / raw)
  To: Ammar Faizi; +Cc: Ahmad Gani, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

Introducing dns parser for better flexibility over the program flow, the
DNS protocol implementation is limited to standard query (OPCODE_QUERY).

Also, Delegate creation of transaction id to the caller, It gives the
caller more flexibility in handling DNS requests.

A single domain name lookup can have two responses: one for IPv4 and one
for IPv6. With this change, the caller may use the same txid for both
queries, making it easier to group related DNS responses.

Signed-off-by: Ahmad Gani <reyuki@gnuweeb.org>
---
 src/gwproxy/dnsparser.c | 581 ++++++++++++++++++++++++++++++++++++++++
 src/gwproxy/dnsparser.h | 192 +++++++++++++
 2 files changed, 773 insertions(+)
 create mode 100644 src/gwproxy/dnsparser.c
 create mode 100644 src/gwproxy/dnsparser.h

diff --git a/src/gwproxy/dnsparser.c b/src/gwproxy/dnsparser.c
new file mode 100644
index 000000000000..cb2a090dfbb0
--- /dev/null
+++ b/src/gwproxy/dnsparser.c
@@ -0,0 +1,581 @@
+#define _DEFAULT_SOURCE
+#include <endian.h>
+#include <stdbool.h>
+#include <gwproxy/dnsparser.h>
+#include <gwproxy/net.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 = 0;
+
+	while (1) {
+		uint8_t c = *p++;
+
+		total++;
+		if (total >= dst_len)
+			return -ENAMETOOLONG;
+
+		if (c == '.' || c == '\0') {
+			if (l < 1 || l > DOMAIN_LABEL_LIMIT)
+				return -EINVAL;
+
+			*lp = (uint8_t)l;
+			lp = sp++;
+			l = 0;
+			if (!c)
+				break;
+		} else {
+			l++;
+			*sp = c;
+			sp++;
+		}
+	}
+
+	return total;
+}
+
+static int calculate_question_len(uint8_t *in, size_t in_len)
+{
+	const uint8_t *p = in;
+	int tot_len, advance_len;
+
+	tot_len = 0;
+	while (true) {
+		if (*p == 0x0) {
+			tot_len++;
+			break;
+		}
+
+		if (tot_len >= (int)in_len)
+			return -ENOBUFS;
+
+		advance_len = *p + 1;
+		tot_len += advance_len;
+		p += advance_len;
+	}
+
+	if (tot_len > DOMAIN_NAME_LIMIT)
+		return -ENAMETOOLONG;
+
+	tot_len += 4;
+	if (tot_len >= (int)in_len)
+		return -ENOBUFS;
+
+	return  tot_len;
+}
+
+static int serialize_answ(uint16_t txid, uint8_t *in, size_t in_len, gwdns_answ_data *out)
+{
+	gwdns_header_pkt *hdr;
+	uint16_t raw_flags;
+	size_t idx, i;
+	void *ptr;
+	int ret;
+
+	idx = sizeof(*hdr);
+	if (idx >= in_len)
+		return -EAGAIN;
+
+	hdr = (void *)in;
+	if (memcmp(&txid, &hdr->id, sizeof(txid)))
+		return -EINVAL;
+
+	memcpy(&raw_flags, &hdr->flags, 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;
+
+	/*
+	 * Check the sizes upfront.
+	 *
+	 * 1 bytes for variable-length
+	 * in[idx] for the length of first name
+	 * 1 bytes for null terminator
+	 * 2 bytes for qtype
+	 * 2 bytes for qclass
+	 */
+	if ((size_t)(1 + in[idx] + 1 + 2 + 2) >= in_len)
+		return -EINVAL;
+
+	ret = calculate_question_len(&in[idx], in_len - idx);
+	if (ret <= 0)
+		return -EINVAL;
+
+	idx += ret;
+	if (idx >= in_len)
+		return -EAGAIN;
+
+	out->hdr.ancount = 0;
+	ptr = malloc(hdr->ancount * sizeof(uint8_t *));
+	if (!ptr)
+		return -ENOMEM;
+
+	out->rr_answ = ptr;
+	for (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;
+		}
+
+		memcpy(&is_compressed, &in[idx], sizeof(is_compressed));
+		is_compressed = ntohs(is_compressed);
+		is_compressed = DNS_IS_COMPRESSED(is_compressed);
+		assert(is_compressed);
+		idx += 2; // NAME
+		if (idx >= in_len) {
+			ret = -EAGAIN;
+			free(item);
+			goto exit_free;
+		}
+
+		memcpy(&item->rr_type, &in[idx], 2);
+		item->rr_type = ntohs(item->rr_type);
+		idx += 2; // TYPE
+		if (idx >= in_len) {
+			ret = -EAGAIN;
+			free(item);
+			goto exit_free;
+		}
+		memcpy(&item->rr_class, &in[idx], 2);
+		item->rr_class = ntohs(item->rr_class);
+		idx += 2; // CLASS
+		if (idx >= in_len) {
+			ret = -EAGAIN;
+			free(item);
+			goto exit_free;
+		}
+		memcpy(&item->ttl, &in[idx], 4);
+		item->ttl = be32toh(item->ttl);
+		idx += 4; // TTL
+		if (idx >= in_len) {
+			ret = -EAGAIN;
+			free(item);
+			goto exit_free;
+		}
+
+		memcpy(&rdlength, &in[idx], sizeof(rdlength));
+		rdlength = ntohs(rdlength);
+		switch (item->rr_type) {
+		case TYPE_AAAA:
+			if (rdlength != sizeof(struct in6_addr)) {
+				ret = -EINVAL;
+				free(item);
+				goto exit_free;
+			}
+			break;
+		case TYPE_A:
+			if (rdlength != sizeof(struct in_addr)) {
+				ret = -EINVAL;
+				free(item);
+				goto exit_free;
+			}
+			break;
+		case TYPE_CNAME:
+			idx += 2 + rdlength;
+			free(item);
+			continue;
+
+		default:
+			ret = -EINVAL;
+			free(item);
+			goto exit_free;
+			break;
+		}
+
+		item->rdlength = rdlength;
+		idx += 2;
+		if (idx >= in_len) {
+			ret = -EAGAIN;
+			free(item);
+			goto exit_free;
+		}
+
+		ptr = malloc(rdlength);
+		if (!ptr) {
+			ret = -ENOMEM;
+			free(item);
+			goto exit_free;
+		}
+
+		memcpy(ptr, &in[idx], rdlength);
+		idx += rdlength;
+		if (idx > in_len) {
+			ret = -EINVAL;
+			free(item);
+			free(ptr);
+			goto exit_free;
+		}
+
+		item->rdata = ptr;
+		out->rr_answ[out->hdr.ancount] = item;
+		out->hdr.ancount++;
+	}
+
+	if (!out->hdr.ancount) {
+		free(out->rr_answ);
+		return -ENODATA;
+	}
+
+	return 0;
+exit_free:
+	for (i = 0; i < out->hdr.ancount; i++) {
+		free(out->rr_answ[i]->rdata);
+		free(out->rr_answ[i]);
+	}
+	free(out->rr_answ);
+	return ret;
+}
+
+static void free_serialize_answ(gwdns_answ_data *answ)
+{
+	size_t i;
+	for (i = 0; i < answ->hdr.ancount; i++) {
+		free(answ->rr_answ[i]->rdata);
+		free(answ->rr_answ[i]);
+	}
+	free(answ->rr_answ);
+}
+
+int gwdns_parse_query(uint16_t txid, const char *service,
+			uint8_t *in, size_t in_len,
+			struct gwdns_addrinfo_node **ai)
+{
+	struct gwdns_addrinfo_node *results, *tail;
+	gwdns_answ_data raw_answ;
+	int r, port;
+	size_t i;
+
+	port = atoi(service);
+	if (port < 0)
+		return -EINVAL;
+	port = htons(port);
+
+	r = serialize_answ(txid, in, in_len, &raw_answ);
+	if (r)
+		return r;
+
+	results = tail = NULL;
+	for (i = 0; i < raw_answ.hdr.ancount; i++) {
+		struct gwdns_addrinfo_node *new_node;
+		gwdns_serialized_answ *answ;
+		struct sockaddr_in6 *i6;
+		struct sockaddr_in *i4;
+
+		answ = raw_answ.rr_answ[i];
+		new_node = malloc(sizeof(*new_node));
+		if (!new_node) {
+			r = -ENOMEM;
+			goto exit_free;
+		}
+		new_node->ai_next = NULL;
+
+		if (answ->rr_type == TYPE_AAAA) {
+			i6 = &new_node->ai_addr.i6;
+			new_node->ai_family = AF_INET6;
+			new_node->ai_addrlen = sizeof(i6);
+			i6->sin6_port = port;
+			i6->sin6_family = AF_INET6;
+			assert(sizeof(i6->sin6_addr) == answ->rdlength);
+			memcpy(&i6->sin6_addr, answ->rdata, answ->rdlength);
+		} else {
+			i4 = &new_node->ai_addr.i4;
+			new_node->ai_family = AF_INET;
+			new_node->ai_addrlen = sizeof(i4);
+			i4->sin_port = port;
+			i4->sin_family = AF_INET;
+			assert(sizeof(i4->sin_addr) == answ->rdlength);
+			memcpy(&i4->sin_addr, answ->rdata, answ->rdlength);
+			new_node->ai_ttl = answ->ttl;
+		}
+
+		if (!tail)
+			results = new_node;
+		else
+			tail->ai_next = new_node;
+		tail = new_node;
+	}
+
+	*ai = results;
+	r = 0;
+exit_free:
+	free_serialize_answ(&raw_answ);
+	return r;
+}
+
+void gwdns_free_parsed_query(struct gwdns_addrinfo_node *ai)
+{
+	struct gwdns_addrinfo_node *tmp, *node = ai;
+	while (node) {
+		tmp = node->ai_next;
+		free(node);
+		node = tmp;
+	}
+}
+
+static 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;
+
+	switch (question->type) {
+	case AF_INET6:
+		question->type = TYPE_AAAA;
+		break;
+	case AF_INET:
+		question->type = TYPE_A;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	hdr = &pkt.hdr;
+	/*
+	* the memset implicitly set opcode to OPCODE_QUERY
+	*/
+	memset(hdr, 0, sizeof(*hdr));
+	/*
+	 * no need to htons, so no ntohs for comparison in serialize_answ.
+	 */
+	hdr->id = question->txid;
+	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;
+}
+
+ssize_t gwdns_build_query(uint16_t txid, const char *name, int family, uint8_t *out, size_t out_len)
+{
+	gwdns_question_part q;
+
+	q.domain = name;
+	q.type = family;
+	q.txid = txid;
+	q.dst_buffer = out;
+	q.dst_len = out_len;
+	return construct_question(&q);
+}
+
+#ifdef RUNTEST
+
+void test_parse_ipv4(void)
+{
+	struct gwdns_addrinfo_node *d, *node;
+	uint16_t txid;
+
+	uint8_t recv_pkt[] = {
+		0x23, 0xc6, 0x81, 0x80, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00,
+		0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00,
+		0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
+		0x00, 0x35, 0x00, 0x04, 0x4a, 0x7d, 0x18, 0x8a, 0xc0, 0x0c, 0x00, 0x01,
+		0x00, 0x01, 0x00, 0x00, 0x00, 0x35, 0x00, 0x04, 0x4a, 0x7d, 0x18, 0x66,
+		0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x35, 0x00, 0x04,
+		0x4a, 0x7d, 0x18, 0x64, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
+		0x00, 0x35, 0x00, 0x04, 0x4a, 0x7d, 0x18, 0x8b, 0xc0, 0x0c, 0x00, 0x01,
+		0x00, 0x01, 0x00, 0x00, 0x00, 0x35, 0x00, 0x04, 0x4a, 0x7d, 0x18, 0x65,
+		0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x35, 0x00, 0x04,
+		0x4a, 0x7d, 0x18, 0x71
+	};
+
+	memcpy(&txid, recv_pkt, 2);
+	d = NULL;
+	assert(!gwdns_parse_query(txid, "80", recv_pkt, sizeof(recv_pkt), &d));
+	assert(d);
+	node = d;
+	while (node) {
+		struct gwdns_addrinfo_node *tmp;
+		char buff[FULL_ADDRSTRLEN];
+
+		tmp = node->ai_next;
+		assert(node->ai_family == AF_INET);
+		convert_ssaddr_to_str(buff, &node->ai_addr);
+		printf("IPv4: %s\n", buff);
+		node = tmp;
+	}
+
+	gwdns_free_parsed_query(d);
+}
+
+void test_parse_ipv6(void)
+{
+	struct gwdns_addrinfo_node *d, *node;
+	uint16_t txid;
+
+	uint8_t recv_pkt[] = {
+		0x45, 0x67,
+		0x81, 0x80,
+		0x00, 0x01,
+		0x00, 0x04,
+		0x00, 0x00,
+		0x00, 0x00,
+
+		0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+		0x03, 0x63, 0x6f, 0x6d,
+		0x00,
+		0x00, 0x1c,
+		0x00, 0x01,
+
+		0xc0, 0x0c,
+		0x00, 0x1c,
+		0x00, 0x01,
+		0x00, 0x00, 0x09, 0x06,
+		0x00, 0x10,
+		0x24, 0x04, 0x68, 0x00, 0x40, 0x03, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71,
+
+		0xc0, 0x0c,
+		0x00, 0x1c, 0x00, 0x01, 0x00, 0x00, 0x09, 0x06,
+		0x00, 0x10,
+		0x24, 0x04, 0x68, 0x00, 0x40, 0x03, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a,
+
+		0xc0, 0x0c,
+		0x00, 0x1c,
+		0x00, 0x01,
+		0x00, 0x00, 0x09, 0x06,
+		0x00, 0x10,
+		0x24, 0x04, 0x68, 0x00, 0x40, 0x03, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65,
+
+		0xc0, 0x0c,
+		0x00, 0x1c,
+		0x00, 0x01,
+		0x00, 0x00, 0x0c, 0x16,
+		0x00, 0x10,
+		0x24, 0x04, 0x68, 0x00, 0x40, 0x03, 0x0c, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71
+	};
+
+	memcpy(&txid, recv_pkt, sizeof(txid));
+
+	node = NULL;
+	assert(!gwdns_parse_query(txid, "80", recv_pkt, sizeof(recv_pkt), &d));
+	assert(d);
+	node = d;
+	while (node) {
+		struct gwdns_addrinfo_node *tmp;
+		char buff[FULL_ADDRSTRLEN];
+
+		tmp = node->ai_next;
+		assert(node->ai_family == AF_INET6);
+		convert_ssaddr_to_str(buff, &node->ai_addr);
+		printf("IPv6: %s\n", buff);
+		node = tmp;
+	}
+
+	gwdns_free_parsed_query(d);
+}
+
+void test_build_ipv4(void)
+{
+	uint8_t buff[UDP_MSG_LIMIT];
+	gwdns_header_pkt *hdr;
+	uint16_t c;
+	ssize_t r;
+
+	c = 0xFFFF;
+	r = gwdns_build_query(c, "google.com", AF_INET, buff, sizeof(buff));
+	assert(r > 0);
+
+	hdr = (void *)buff;
+	assert(ntohs(hdr->qdcount) == 1);
+	assert(!hdr->nscount);
+	assert(!DNS_QR(hdr->flags));
+	assert(DNS_OPCODE(hdr->flags) == OPCODE_QUERY);
+	c = htons(TYPE_A);
+	assert(!memcmp(buff + 12 + 12, &c, 2));
+}
+
+void test_build_ipv6(void)
+{
+	uint8_t buff[UDP_MSG_LIMIT];
+	gwdns_header_pkt *hdr;
+	uint16_t c;
+	ssize_t r;
+
+	c = 0xFFFF;
+	r = gwdns_build_query(c, "google.com", AF_INET6, buff, sizeof(buff));
+	assert(r > 0);
+
+	hdr = (void *)buff;
+	assert(ntohs(hdr->qdcount) == 1);
+	assert(!hdr->nscount);
+	assert(!DNS_QR(hdr->flags));
+	assert(DNS_OPCODE(hdr->flags) == OPCODE_QUERY);
+	c = htons(TYPE_AAAA);
+	assert(!memcmp(buff + 12 + 12, &c, 2));
+}
+
+/*
+ * test mock data of recv in both IPv4 and IPv6
+ *
+ * the mock data are produced by this script:
+ * https://gist.github.com/realyukii/d7b450b4ddc305c66a2d8cd5600f23c4
+ */
+void run_all_tests(void)
+{
+	/*
+	 * We can't use serialize_answ to parse multiple response at once.
+	 * The caller MUST call serialize_answ more than one time if there's
+	 * more than one response, because txid is passed to only verify one
+	 * response.
+	 */
+	fprintf(stderr, "test constructing DNS standard query packet for TYPE_A!\n");
+	test_build_ipv4();
+	fprintf(stderr, "test constructing DNS standard query packet for TYPE_AAAA!\n");
+	test_build_ipv6();
+	fprintf(stderr, "test parsing DNS standard query packet for TYPE_A!\n");
+	test_parse_ipv4();
+	fprintf(stderr, "test parsing DNS standard query packet for TYPE_AAAA!\n");
+	test_parse_ipv6();
+	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..6d5769cc3259
--- /dev/null
+++ b/src/gwproxy/dnsparser.h
@@ -0,0 +1,192 @@
+#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 <gwproxy/net.h>
+
+#ifndef GWP_DNS_PARSER_H
+#define GWP_DNS_PARSER_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)
+} gwdns_op;
+
+typedef enum {
+	TYPE_A		= 1,	// IPv4 host address
+	TYPE_CNAME	= 5,	// the canonical name for an alias
+	TYPE_AAAA	= 28,	// IPv6 host address
+} gwdns_type;
+
+typedef enum {
+	CLASS_IN	= 1,	// Internet
+} 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 txid;
+	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;
+
+struct gwdns_addrinfo_node {
+	int				ai_family;
+	int				ai_ttl;
+	socklen_t			ai_addrlen;
+	struct gwp_sockaddr		ai_addr;
+	struct gwdns_addrinfo_node	*ai_next;
+};
+
+/*
+ * Build standard query for domain name lookup.
+ *
+ * The caller may need to check for potential transaction ID collisions.
+ *
+ * Possible errors are:
+ * - ENAMETOOLONG	name is too long.
+ * - ENOBUFS		length specified by out_len is not sufficient.
+ * - EINVAL		malformed name or unsupported value of family.
+ *
+ * @param txid		transaction id
+ * @param name		domain name
+ * @param family	choose request for IPv4 or IPv6
+ * @param out		destination buffer for constructed packet
+ * @param out_len	available capacity of destination buffer
+ * @return		length of bytes written into dst_buffer on success,
+ * 			or a negative integer on failure.
+ */
+ssize_t gwdns_build_query(uint16_t txid, const char *name, int family, uint8_t *out, size_t out_len);
+
+/*
+ * Parse name server's answer
+ *
+ * Possible errors 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 service	port number in ascii
+ * @param in		a pointer to buffer that need to be parsed
+ * @param in_len	a pointer to buffer that need to be parsed
+ * @param ai		a pointer to address info
+ * @return		zero on success or a negative number on failure
+ */
+int gwdns_parse_query(uint16_t txid, const char *service,
+			uint8_t *in, size_t in_len,
+			struct gwdns_addrinfo_node **ai);
+void gwdns_free_parsed_query(struct gwdns_addrinfo_node *addrinfo);
+
+#endif /* #ifndef GWP_DNS_PARSER_H */
-- 
Ahmad Gani


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-10 10:43 [PATCH gwproxy v10 0/2] Initial work on integration of DNS parser lib in gwproxy Ahmad Gani
  2025-09-10 10:43 ` [PATCH gwproxy v10 1/2] dnsparser: Add dns parser code Ahmad Gani
@ 2025-09-10 10:43 ` Ahmad Gani
  2025-09-11 10:11   ` Alviro Iskandar Setiawan
  1 sibling, 1 reply; 13+ messages in thread
From: Ahmad Gani @ 2025-09-10 10:43 UTC (permalink / raw)
  To: Ammar Faizi; +Cc: Ahmad Gani, Alviro Iskandar Setiawan, GNU/Weeb Mailing List

The raw DNS backend is now marked as experimental and disabled by default.
It can be enabled at build time with the configure option --use-raw-dns,
and at runtime with the gwproxy option -r 1.

Add fallback mechanism for raw DNS
If *_PREFER_* is used in the restyp, the raw DNS backend will attempt to
retry DNS query with different address family.

Add DNS server option (remove hard-coded DNS server)
Introduce --dns-server gwproxy' cmdline option instead of hard-coded
value.

Signed-off-by: Ahmad Gani <reyuki@gnuweeb.org>
---
 Makefile                |   2 +-
 configure               |   8 ++
 src/gwproxy/common.h    |   4 +
 src/gwproxy/dns.c       | 254 ++++++++++++++++++++++++++++++++++------
 src/gwproxy/dns.h       |  58 ++++++++-
 src/gwproxy/dnsparser.c |   2 +
 src/gwproxy/ev/epoll.c  | 128 ++++++++++++++++++--
 src/gwproxy/gwproxy.c   | 213 +++++++++++++++++++++++++++++++--
 src/gwproxy/gwproxy.h   |  89 +++++++++++++-
 9 files changed, 691 insertions(+), 67 deletions(-)

diff --git a/Makefile b/Makefile
index 3ac35f052793..eb750e0fc31c 100644
--- a/Makefile
+++ b/Makefile
@@ -43,7 +43,7 @@ LIBGWPSOCKS5_TEST_CC_SOURCES = $(GWPROXY_DIR)/tests/socks5.c
 LIBGWPSOCKS5_TEST_OBJECTS = $(LIBGWPSOCKS5_TEST_CC_SOURCES:%.c=%.c.o)
 
 LIBGWDNS_TARGET = libgwdns.so
-LIBGWDNS_CC_SOURCES = $(GWPROXY_DIR)/dns.c $(GWPROXY_DIR)/dns_cache.c
+LIBGWDNS_CC_SOURCES = $(GWPROXY_DIR)/dns.c $(GWPROXY_DIR)/dnsparser.c $(GWPROXY_DIR)/dns_cache.c $(GWPROXY_DIR)/net.c
 LIBGWDNS_OBJECTS = $(LIBGWDNS_CC_SOURCES:%.c=%.c.o)
 LIBGWDNS_TEST_TARGET = $(GWPROXY_DIR)/tests/dns.t
 LIBGWDNS_TEST_CC_SOURCES = $(GWPROXY_DIR)/tests/dns.c
diff --git a/configure b/configure
index 44c49da20de4..1cc61e7691da 100755
--- a/configure
+++ b/configure
@@ -34,6 +34,9 @@ for opt do
 	--cxx=*)
 		cxx="$optarg";
 	;;
+	--use-raw-dns)
+		use_raw_dns="yes";
+	;;
 	--use-io-uring)
 		use_io_uring="yes";
 	;;
@@ -87,6 +90,7 @@ Options: [defaults in brackets after descriptions]
   --cxx=CMD                Use CMD as the C++ compiler
   --debug                  Build with debug enabled
   --use-io-uring           Enable io_uring support (default: no)
+  --use-raw-dns            Enable experimental raw DNS backend (default: no)
   --sanitize               Enable sanitizers (default: no)
 EOF
 exit 0;
@@ -333,6 +337,10 @@ if test "${use_io_uring}" = "yes"; then
 	CXXFLAGS="${CXXFLAGS} -I./src/liburing/src/include";
 fi;
 
+if test "${use_raw_dns}" = "yes"; then
+	add_config "CONFIG_RAW_DNS";
+fi;
+
 if test "${use_sanitize}" = "yes"; then
 	add_config "CONFIG_SANITIZE";
 	if ! add_c_flag "-fsanitize=address"; then
diff --git a/src/gwproxy/common.h b/src/gwproxy/common.h
index 8ae6ab4a31f5..aae4446a6875 100644
--- a/src/gwproxy/common.h
+++ b/src/gwproxy/common.h
@@ -12,6 +12,10 @@
 #define unlikely(x)	__builtin_expect(!!(x), 0)
 #endif
 
+#ifndef __maybe_unused
+#define __maybe_unused	__attribute__((__unused__))
+#endif
+
 #ifndef __cold
 #define __cold		__attribute__((__cold__))
 #endif
diff --git a/src/gwproxy/dns.c b/src/gwproxy/dns.c
index cdc62a5bcf78..07a9332a31e6 100644
--- a/src/gwproxy/dns.c
+++ b/src/gwproxy/dns.c
@@ -23,8 +23,7 @@
 
 #include <pthread.h>
 #include <sys/eventfd.h>
-
-struct gwp_dns_ctx;
+#include <gwproxy/dnsparser.h>
 
 struct gwp_dns_wrk {
 	struct gwp_dns_ctx	*ctx;
@@ -32,20 +31,6 @@ struct gwp_dns_wrk {
 	pthread_t		thread;
 };
 
-struct gwp_dns_ctx {
-	volatile bool		should_stop;
-	pthread_mutex_t		lock;
-	pthread_cond_t		cond;
-	uint32_t		nr_sleeping;
-	uint32_t		nr_entries;
-	struct gwp_dns_entry	*head;
-	struct gwp_dns_entry	*tail;
-	struct gwp_dns_wrk	*workers;
-	struct gwp_dns_cache	*cache;
-	time_t			last_scan;
-	struct gwp_dns_cfg	cfg;
-};
-
 static void put_all_entries(struct gwp_dns_entry *head)
 {
 	struct gwp_dns_entry *e, *next;
@@ -182,13 +167,181 @@ int gwp_dns_resolve(struct gwp_dns_ctx *ctx, const char *name,
 	return found ? 0 : -EHOSTUNREACH;
 }
 
+#ifdef CONFIG_RAW_DNS
+
+static void _gwp_dns_entry_free(struct gwp_dns_entry *e)
+{
+	assert(e);
+	free(e->name);
+	free(e);
+}
+
+void gwp_dns_raw_entry_free(struct gwp_dns_ctx *ctx, struct gwp_dns_entry *e)
+{
+	struct gwp_dns_entry *new_e;
+
+	assert(e);
+
+	new_e = ctx->entries[--ctx->nr_entries];
+	assert(ctx->nr_entries == new_e->idx);
+	new_e->idx = e->idx;
+	ctx->entries[e->idx] = new_e;
+	ctx->entries[ctx->nr_entries] = NULL;
+
+	_gwp_dns_entry_free(e);
+}
+
+static void free_all_queued_entries(struct gwp_dns_ctx *ctx)
+{
+	uint32_t i;
+	for (i = 0; i < ctx->nr_entries; i++) {
+		struct gwp_dns_entry *e = ctx->entries[i];
+		_gwp_dns_entry_free(e);
+	}
+
+	free(ctx->entries);
+}
+
+static bool realloc_entries(struct gwp_dns_ctx *ctx)
+{
+	struct gwp_dns_entry **tmp;
+	int new_cap;
+
+	new_cap = ctx->entry_cap * 2;
+	tmp = realloc(ctx->entries, new_cap * sizeof(*tmp));
+	if (!tmp)
+		return 1;
+
+	ctx->entries = tmp;
+	ctx->entry_cap = new_cap;
+
+	return 0;
+}
+
+static int attempt_fallback(struct gwp_dns_ctx *ctx, struct gwp_dns_entry *e)
+{
+	int af, r;
+
+	/* Fallback to other family if this one yield no results */
+	switch (ctx->cfg.restyp) {
+	case GWP_DNS_RESTYP_PREFER_IPV4:
+		af = AF_INET6;
+		break;
+	case GWP_DNS_RESTYP_PREFER_IPV6:
+		af = AF_INET;
+		break;
+	default:
+		assert(0);
+		return -EINVAL;
+	}
+
+	r = (int)gwdns_build_query(e->txid,
+				e->name, af, e->payload, sizeof(e->payload));
+	if (r > 0) {
+		e->payloadlen = r;
+		r = -EAGAIN;
+	}
+
+	return r;
+}
+
+int gwp_dns_process(uint8_t buff[UDP_MSG_LIMIT], int bufflen, struct gwp_dns_ctx *ctx, struct gwp_dns_entry *e)
+{
+	struct gwdns_addrinfo_node *ai;
+	ssize_t r;
+
+	r = gwdns_parse_query(e->txid, e->service, buff, bufflen, &ai);
+	if (r) {
+		if (r == -ENODATA)
+			r = attempt_fallback(ctx, e);
+		goto exit;
+	}
+
+	e->addr = ai->ai_addr;
+
+	gwdns_free_parsed_query(ai);
+exit:
+	return (int)r;
+}
+
+struct gwp_dns_entry *gwp_raw_dns_queue(uint16_t txid, struct gwp_dns_ctx *ctx,
+				    const char *name, const char *service)
+{
+	struct gwp_dns_entry *e;
+	size_t nl, sl;
+	ssize_t r;
+	int af;
+
+	e = malloc(sizeof(*e));
+	if (!e)
+		return NULL;
+
+	if (ctx->nr_entries == ctx->entry_cap && realloc_entries(ctx))
+		return NULL;
+
+	switch (ctx->cfg.restyp) {
+	case GWP_DNS_RESTYP_PREFER_IPV4:
+	case GWP_DNS_RESTYP_IPV4_ONLY:
+	case GWP_DNS_RESTYP_DEFAULT:
+		af = AF_INET;
+		break;
+	case GWP_DNS_RESTYP_PREFER_IPV6:
+	case GWP_DNS_RESTYP_IPV6_ONLY:
+		af = AF_INET6;
+		break;
+	default:
+		assert(0);
+		goto out_free_e;
+	}
+
+	r = gwdns_build_query(txid,
+				name, af, e->payload, sizeof(e->payload));
+	if (r < 0)
+		goto out_free_e;
+	e->payloadlen = (int)r;
+
+	/*
+	 * Merge name and service into a single allocated string to
+	 * avoid multiple allocations.
+	 *
+	 * The format is: "<name>\0<service>\0" where both name and
+	 * service are null-terminated strings.
+	 */
+	nl = strlen(name);
+	sl = service ? strlen(service) : 0;
+	e->name = malloc(nl + 1 + sl + 1);
+	if (!e->name)
+		goto out_free_e;
+
+	e->service = e->name + nl + 1;
+	memcpy(e->name, name, nl + 1);
+	if (service)
+		memcpy(e->service, service, sl + 1);
+	else
+		e->service[0] = '\0';
+
+	e->res = 0;
+	e->idx = ctx->nr_entries++;
+	ctx->entries[e->idx] = e;
+
+	return e;
+out_free_e:
+	free(e);
+	return NULL;
+}
+#else
+static void free_all_queued_entries(__maybe_unused struct gwp_dns_ctx *ctx)
+{
+}
+#endif /* #ifdef CONFIG_RAW_DNS */
+
 static void gwp_dns_entry_free(struct gwp_dns_entry *e)
 {
 	if (!e)
 		return;
 
 	assert(e->ev_fd >= 0);
-	close(e->ev_fd);
+	__sys_close(e->ev_fd);
 	free(e->name);
 	free(e);
 }
@@ -719,33 +872,51 @@ int gwp_dns_ctx_init(struct gwp_dns_ctx **ctx_p, const struct gwp_dns_cfg *cfg)
 		goto out_free_ctx;
 	}
 
-	r = pthread_cond_init(&ctx->cond, NULL);
-	if (r) {
-		r = -r;
-		goto out_destroy_mutex;
+	if (cfg->use_raw_dns) {
+#ifdef CONFIG_RAW_DNS
+		r = convert_str_to_ssaddr(cfg->ns_addr_str, &ctx->ns_addr, 53);
+		if (r)
+			goto out_destroy_mutex;
+		ctx->ns_addrlen = ctx->ns_addr.sa.sa_family == AF_INET
+				? sizeof(ctx->ns_addr.i4)
+				: sizeof(ctx->ns_addr.i6);
+		ctx->entry_cap = DEFAULT_ENTRIES_CAP;
+		ctx->entries = malloc(ctx->entry_cap * sizeof(*ctx->entries));
+		if (!ctx->entries) {
+			__sys_close(r);
+			r = -ENOMEM;
+			goto out_destroy_mutex;
+		}
+#endif
+	} else {
+		r = pthread_cond_init(&ctx->cond, NULL);
+		if (r) {
+			r = -r;
+			goto out_destroy_mutex;
+		}
+
+		ctx->nr_sleeping = 0;
+		ctx->workers = NULL;
+		ctx->head = NULL;
+		ctx->tail = NULL;
+		r = init_workers(ctx);
+		if (r)
+			goto out_destroy_cond;
 	}
+	ctx->nr_entries = 0;
 
 	r = init_cache(ctx);
 	if (r)
 		goto out_destroy_cond;
 
-	ctx->nr_sleeping = 0;
-	ctx->nr_entries = 0;
-	ctx->workers = NULL;
-	ctx->head = NULL;
-	ctx->tail = NULL;
 	ctx->should_stop = false;
 	ctx->last_scan = time(NULL);
-	r = init_workers(ctx);
-	if (r)
-		goto out_free_cache;
 
 	*ctx_p = ctx;
 	return 0;
-out_free_cache:
-	free_cache(ctx->cache);
 out_destroy_cond:
-	pthread_cond_destroy(&ctx->cond);
+	if (!cfg->use_raw_dns)
+		pthread_cond_destroy(&ctx->cond);
 out_destroy_mutex:
 	pthread_mutex_destroy(&ctx->lock);
 out_free_ctx:
@@ -762,10 +933,14 @@ static void put_all_queued_entries(struct gwp_dns_ctx *ctx)
 
 void gwp_dns_ctx_free(struct gwp_dns_ctx *ctx)
 {
-	free_workers(ctx);
+	if (ctx->cfg.use_raw_dns) {
+		free_all_queued_entries(ctx);
+	} else {
+		free_workers(ctx);
+		pthread_cond_destroy(&ctx->cond);
+		put_all_queued_entries(ctx);
+	}
 	pthread_mutex_destroy(&ctx->lock);
-	pthread_cond_destroy(&ctx->cond);
-	put_all_queued_entries(ctx);
 	free_cache(ctx->cache);
 	free(ctx);
 }
@@ -811,7 +986,7 @@ struct gwp_dns_entry *gwp_dns_queue(struct gwp_dns_ctx *ctx,
 	sl = service ? strlen(service) : 0;
 	e->name = malloc(nl + 1 + sl + 1);
 	if (!e->name)
-		goto out_close_ev_fd;
+		goto out_close_fd;
 
 	e->service = e->name + nl + 1;
 	memcpy(e->name, name, nl + 1);
@@ -820,13 +995,14 @@ struct gwp_dns_entry *gwp_dns_queue(struct gwp_dns_ctx *ctx,
 	else
 		e->service[0] = '\0';
 
-	atomic_init(&e->refcnt, 2);
 	e->res = 0;
+	atomic_init(&e->refcnt, 2);
 	push_queue(ctx, e);
+
 	return e;
 
-out_close_ev_fd:
-	close(e->ev_fd);
+out_close_fd:
+	__sys_close(e->ev_fd);
 out_free_e:
 	free(e);
 	return NULL;
diff --git a/src/gwproxy/dns.h b/src/gwproxy/dns.h
index 10c7cea2ebe4..049505997e37 100644
--- a/src/gwproxy/dns.h
+++ b/src/gwproxy/dns.h
@@ -9,11 +9,20 @@
 #include <stdatomic.h>
 #include <stdbool.h>
 #include <netinet/in.h>
+#include <gwproxy/common.h>
 #include <gwproxy/net.h>
-
-struct gwp_dns_wrk;
+#include <gwproxy/dnsparser.h>
+#include <gwproxy/syscall.h>
 
 struct gwp_dns_entry {
+#ifdef CONFIG_RAW_DNS
+	uint32_t		idx;
+	int			payloadlen;
+	union {
+		uint16_t	txid;
+		uint8_t		payload[UDP_MSG_LIMIT];
+	};
+#endif
 	char			*name;
 	char			*service;
 	_Atomic(int)		refcnt;
@@ -31,13 +40,37 @@ enum {
 	GWP_DNS_RESTYP_PREFER_IPV6	= 4,
 };
 
+#define DEFAULT_ENTRIES_CAP 255
+
 struct gwp_dns_cfg {
 	int		cache_expiry;	/* In seconds. <= 0 to disable cache. */
 	uint32_t	nr_workers;
 	uint32_t	restyp;
+	bool		use_raw_dns;
+#ifdef CONFIG_RAW_DNS
+	const char	*ns_addr_str;
+#endif
 };
 
-struct gwp_dns_ctx;
+struct gwp_dns_ctx {
+#ifdef CONFIG_RAW_DNS
+	uint32_t		entry_cap;
+	struct gwp_dns_entry	**entries;
+	struct gwp_sockaddr	ns_addr;
+	socklen_t		ns_addrlen;
+#endif
+	volatile bool		should_stop;
+	pthread_mutex_t		lock;
+	pthread_cond_t		cond;
+	uint32_t		nr_sleeping;
+	uint32_t		nr_entries;
+	struct gwp_dns_entry	*head;
+	struct gwp_dns_entry	*tail;
+	struct gwp_dns_wrk	*workers;
+	struct gwp_dns_cache	*cache;
+	time_t			last_scan;
+	struct gwp_dns_cfg	cfg;
+};
 
 /**
  * Initialize the DNS context. Stores the context in `*ctx_p`. When
@@ -87,6 +120,25 @@ struct gwp_dns_entry *gwp_dns_queue(struct gwp_dns_ctx *ctx,
  */
 bool gwp_dns_entry_put(struct gwp_dns_entry *entry);
 
+#ifdef CONFIG_RAW_DNS
+struct gwp_dns_entry *gwp_raw_dns_queue(uint16_t txid, struct gwp_dns_ctx *ctx,
+				    const char *name, const char *service);
+
+void gwp_dns_raw_entry_free(struct gwp_dns_ctx *ctx, struct gwp_dns_entry *e);
+
+int gwp_dns_process(uint8_t buff[UDP_MSG_LIMIT], int bufflen, struct gwp_dns_ctx *ctx, struct gwp_dns_entry *e);
+#else
+static inline struct gwp_dns_entry *gwp_raw_dns_queue(__maybe_unused uint16_t txid, __maybe_unused struct gwp_dns_ctx *ctx,
+				    __maybe_unused const char *name, __maybe_unused const char *service)
+{
+	return NULL;
+}
+
+static inline void gwp_dns_raw_entry_free(__maybe_unused struct gwp_dns_ctx *ctx, __maybe_unused struct gwp_dns_entry *e)
+{
+}
+#endif
+
 /**
  * Lookup a DNS entry in the cache. If the entry is found, it fills the
  * `addr` structure with the resolved address and returns 0. If the entry is
diff --git a/src/gwproxy/dnsparser.c b/src/gwproxy/dnsparser.c
index cb2a090dfbb0..973a189bed19 100644
--- a/src/gwproxy/dnsparser.c
+++ b/src/gwproxy/dnsparser.c
@@ -318,6 +318,8 @@ int gwdns_parse_query(uint16_t txid, const char *service,
 	*ai = results;
 	r = 0;
 exit_free:
+	if (r && results)
+		gwdns_free_parsed_query(results);
 	free_serialize_answ(&raw_answ);
 	return r;
 }
diff --git a/src/gwproxy/ev/epoll.c b/src/gwproxy/ev/epoll.c
index d46568a6a2b1..48398442c5a7 100644
--- a/src/gwproxy/ev/epoll.c
+++ b/src/gwproxy/ev/epoll.c
@@ -39,6 +39,16 @@ int gwp_ctx_init_thread_epoll(struct gwp_wrk *w)
 		goto out_close_ep_fd;
 	}
 
+	if (w->ctx->cfg.use_raw_dns) {
+#ifdef CONFIG_RAW_DNS
+		ev.events = EPOLLIN;
+		ev.data.u64 = EV_BIT_DNS_QUERY;
+		r = __sys_epoll_ctl(ep_fd, EPOLL_CTL_ADD, w->dns_resolver.udp_fd, &ev);
+		if (unlikely(r))
+			return (int)r;
+#endif
+	}
+
 	w->evsz = 512;
 	events = calloc(w->evsz, sizeof(*events));
 	if (!events) {
@@ -778,6 +788,79 @@ static int handle_connect(struct gwp_wrk *w, struct gwp_conn_pair *gcp)
 	return 0;
 }
 
+#ifdef CONFIG_RAW_DNS
+static int send_raw_dns_query(struct gwp_wrk *w, struct gwp_conn_pair *gcp)
+{
+	struct gwp_dns_entry *gde = gcp->gde;
+	struct gwp_dns_ctx *dctx;
+	ssize_t r;
+
+	dctx = w->ctx->dns;
+	r = __sys_sendto(
+		w->dns_resolver.udp_fd, gde->payload, gde->payloadlen, MSG_NOSIGNAL,
+		&dctx->ns_addr.sa, dctx->ns_addrlen
+	);
+	if (r < 0)
+		return (int)r;
+	pr_dbg(&w->ctx->lh, "standard DNS query for %s has been sent", gde->name);
+
+	return 0;
+}
+static int read_raw_dns(struct gwp_wrk *w, struct gwp_conn_pair **pgcp, struct gwp_dns_entry **pgde)
+{
+	uint8_t buff[UDP_MSG_LIMIT];
+	struct gwp_conn_pair *gcp;
+	struct gwp_dns_entry *gde;
+	struct gwp_dns_ctx *dctx;
+	uint16_t txid;
+	int r;
+
+	dctx = w->ctx->dns;
+	r = (int)__sys_recvfrom(
+		w->dns_resolver.udp_fd, buff, sizeof(buff), 0,
+		&dctx->ns_addr.sa, &dctx->ns_addrlen
+	);
+	if (r < 0)
+		return (int)r;
+	if (r < 38)
+		return -EINVAL;
+
+	memcpy(&txid, buff, 2);
+	gcp = w->dns_resolver.sess_map[txid];
+	if (!gcp)
+		return -ENOENT;
+
+	gde = gcp->gde;
+	pr_dbg(&w->ctx->lh, "proxy session for DNS query %s was found!", gde->name);
+	r = gwp_dns_process(buff, r, dctx, gde);
+	if (r == -EAGAIN) {
+		pr_dbg(&w->ctx->lh, "DNS Fallback");
+		return send_raw_dns_query(w, gcp);
+	} else if (r)
+		gde->res = r;
+
+	r = return_txid(w, gde);
+	assert(!r);
+	if (r)
+		return r;
+
+	*pgcp = gcp;
+	*pgde = gde;
+	return 0;
+}
+#else
+static int send_raw_dns_query(__maybe_unused struct gwp_wrk *w, __maybe_unused struct gwp_conn_pair *gcp)
+{
+	return 0;
+}
+static int read_raw_dns(__maybe_unused struct gwp_wrk *w,
+			__maybe_unused struct gwp_conn_pair **pgcp,
+			__maybe_unused struct gwp_dns_entry **pgde)
+{
+	return 0;
+}
+#endif
+
 static int arm_poll_for_dns_query(struct gwp_wrk *w,
 					struct gwp_conn_pair *gcp)
 {
@@ -786,16 +869,22 @@ static int arm_poll_for_dns_query(struct gwp_wrk *w,
 	int r;
 
 	assert(gde);
-	assert(gde->ev_fd >= 0);
 
 	ev.events = EPOLLIN;
 	ev.data.u64 = 0;
 	ev.data.ptr = gcp;
 	ev.data.u64 |= EV_BIT_DNS_QUERY;
 
-	r = __sys_epoll_ctl(w->ep_fd, EPOLL_CTL_ADD, gde->ev_fd, &ev);
-	if (unlikely(r))
-		return r;
+	if (w->ctx->cfg.use_raw_dns) {
+		r = send_raw_dns_query(w, gcp);
+		if (r)
+			return r;
+	} else {
+		assert(gde->ev_fd >= 0);
+		r = __sys_epoll_ctl(w->ep_fd, EPOLL_CTL_ADD, gde->ev_fd, &ev);
+		if (unlikely(r))
+			return r;
+	}
 
 	return 0;
 }
@@ -822,18 +911,26 @@ static void log_dns_query(struct gwp_wrk *w, struct gwp_conn_pair *gcp,
 __hot
 static int handle_ev_dns_query(struct gwp_wrk *w, struct gwp_conn_pair *gcp)
 {
-	struct gwp_dns_entry *gde = gcp->gde;
-	int r, ct = gcp->conn_state;
+	struct gwp_dns_entry *gde = NULL;
+	int r, ct;
 
-	assert(gde);
-	assert(gde->ev_fd >= 0);
+	if (w->ctx->cfg.use_raw_dns) {
+		r = read_raw_dns(w, &gcp, &gde);
+		if (r)
+			return r;
+	} else {
+		gde = gcp->gde;
+		assert(gde);
+		assert(gde->ev_fd >= 0);
+		r = __sys_epoll_ctl(w->ep_fd, EPOLL_CTL_DEL, gde->ev_fd, NULL);
+		if (unlikely(r))
+			return r;
+	}
+
+	ct = gcp->conn_state;
 	assert(ct == CONN_STATE_SOCKS5_DNS_QUERY ||
 	       ct == CONN_STATE_HTTP_DNS_QUERY);
 
-	r = __sys_epoll_ctl(w->ep_fd, EPOLL_CTL_DEL, gde->ev_fd, NULL);
-	if (unlikely(r))
-		return r;
-
 	log_dns_query(w, gcp, gde);
 	if (likely(!gde->res)) {
 		gcp->target_addr = gde->addr;
@@ -845,7 +942,12 @@ static int handle_ev_dns_query(struct gwp_wrk *w, struct gwp_conn_pair *gcp)
 			r = -EIO;
 	}
 
-	gwp_dns_entry_put(gde);
+	if (w->ctx->cfg.use_raw_dns) {
+		pr_dbg(&w->ctx->lh, "removing proxy session from the %s's txid map", gde->name);
+		gwp_dns_raw_entry_free(w->ctx->dns, gde);
+	} else {
+		gwp_dns_entry_put(gde);
+	}
 	gcp->gde = NULL;
 	return r;
 }
diff --git a/src/gwproxy/gwproxy.c b/src/gwproxy/gwproxy.c
index 3d277f7ee6f8..8271ae95143a 100644
--- a/src/gwproxy/gwproxy.c
+++ b/src/gwproxy/gwproxy.c
@@ -44,6 +44,11 @@
 
 static const struct option long_opts[] = {
 	{ "help",		no_argument,		NULL,	'h' },
+	{ "raw-dns",		required_argument,	NULL,	'r' },
+#ifdef CONFIG_RAW_DNS
+	{ "dns-server",		required_argument,	NULL,	'j' },
+	{ "session-map-cap",	required_argument,	NULL,	's' },
+#endif
 	{ "event-loop",		required_argument,	NULL,	'e' },
 	{ "bind",		required_argument,	NULL,	'b' },
 	{ "target",		required_argument,	NULL,	't' },
@@ -54,7 +59,6 @@ static const struct option long_opts[] = {
 	{ "socks5-auth-file",	required_argument,	NULL,	'A' },
 	{ "socks5-dns-cache-secs",	required_argument,	NULL,	'L' },
 	{ "nr-workers",		required_argument,	NULL,	'w' },
-	{ "nr-dns-workers",	required_argument,	NULL,	'W' },
 	{ "connect-timeout",	required_argument,	NULL,	'c' },
 	{ "target-buf-size",	required_argument,	NULL,	'T' },
 	{ "client-buf-size",	required_argument,	NULL,	'C' },
@@ -74,6 +78,7 @@ static const struct gwp_cfg default_opts = {
 	.event_loop		= "epoll",
 	.bind			= "[::]:1080",
 	.target			= NULL,
+	.use_raw_dns		= false,
 	.as_socks5		= false,
 	.as_http		= false,
 	.socks5_prefer_ipv6	= false,
@@ -81,7 +86,6 @@ static const struct gwp_cfg default_opts = {
 	.socks5_auth_file	= NULL,
 	.socks5_dns_cache_secs	= 0,
 	.nr_workers		= 4,
-	.nr_dns_workers		= 4,
 	.connect_timeout	= 5,
 	.target_buf_size	= 2048,
 	.client_buf_size	= 2048,
@@ -94,6 +98,10 @@ static const struct gwp_cfg default_opts = {
 	.log_level		= 3,
 	.log_file		= "/dev/stdout",
 	.pid_file		= NULL,
+#ifdef CONFIG_RAW_DNS
+	.ns_addr_str		= "1.1.1.1",
+	.sess_map_cap		= 16
+#endif
 };
 
 __cold
@@ -104,6 +112,10 @@ static void show_help(const char *app)
 	printf("  -h, --help                      Show this help message and exit\n");
 	printf("  -e, --event-loop=name           Specify the event loop to use (default: %s)\n", default_opts.event_loop);
 	printf("                                  Available values: epoll, io_uring\n");
+	printf("  -r, --raw-dns=0|1               Use experimental raw DNS as the backend (default: %d)\n", default_opts.use_raw_dns);
+#ifdef CONFIG_RAW_DNS
+	printf("  -j, --dns-server=addr           DNS server address (default: %s)\n", default_opts.ns_addr_str);
+#endif
 	printf("  -b, --bind=addr:port            Bind to the specified address (default: %s)\n", default_opts.bind);
 	printf("  -t, --target=addr_port          Target address to connect to\n");
 	printf("  -S, --as-socks5=0|1             Run as a SOCKS5 proxy (default: %d)\n", default_opts.as_socks5);
@@ -114,7 +126,6 @@ static void show_help(const char *app)
 	printf("  -L, --socks5-dns-cache-secs=sec SOCKS5 DNS cache duration in seconds (default: %d)\n", default_opts.socks5_dns_cache_secs);
 	printf("                                  Set to 0 or a negative number to disable DNS caching.\n");
 	printf("  -w, --nr-workers=nr             Number of worker threads (default: %d)\n", default_opts.nr_workers);
-	printf("  -W, --nr-dns-workers=nr         Number of DNS worker threads for SOCKS5 (default: %d)\n", default_opts.nr_dns_workers);
 	printf("  -c, --connect-timeout=sec       Connection to target timeout in seconds (default: %d)\n", default_opts.connect_timeout);
 	printf("  -T, --target-buf-size=nr        Target buffer size in bytes (default: %d)\n", default_opts.target_buf_size);
 	printf("  -C, --client-buf-size=nr        Client buffer size in bytes (default: %d)\n", default_opts.client_buf_size);
@@ -159,6 +170,9 @@ static int parse_options(int argc, char *argv[], struct gwp_cfg *cfg)
 		case 'h':
 			show_help(argv[0]);
 			exit(0);
+		case 'r':
+			cfg->use_raw_dns = !!atoi(optarg);
+			break;
 		case 'e':
 			cfg->event_loop = optarg;
 			break;
@@ -189,9 +203,6 @@ static int parse_options(int argc, char *argv[], struct gwp_cfg *cfg)
 		case 'w':
 			cfg->nr_workers = atoi(optarg);
 			break;
-		case 'W':
-			cfg->nr_dns_workers = atoi(optarg);
-			break;
 		case 'c':
 			cfg->connect_timeout = atoi(optarg);
 			break;
@@ -228,6 +239,14 @@ static int parse_options(int argc, char *argv[], struct gwp_cfg *cfg)
 		case 'p':
 			cfg->pid_file = optarg;
 			break;
+#ifdef CONFIG_RAW_DNS
+		case 'j':
+			cfg->ns_addr_str = optarg;
+			break;
+		case 's':
+			cfg->sess_map_cap = atoi(optarg);
+			break;
+#endif
 		default:
 			fprintf(stderr, "Unknown option: %c\n", c);
 			show_help(argv[0]);
@@ -446,6 +465,143 @@ static void gwp_ctx_free_thread_event(struct gwp_wrk *w)
 	}
 }
 
+#ifdef CONFIG_RAW_DNS
+static inline int realloc_stack(struct dns_resolver *resolv)
+{
+	struct gwp_conn_pair **cp;
+	uint16_t *stack;
+	size_t newcap;
+	uint16_t j, idx;
+
+	newcap = resolv->sess_map_cap * 2;
+	if (newcap > 65536)
+		return -ENOSPC;
+
+	cp = realloc(resolv->sess_map, sizeof(*cp) * newcap);
+	if (!cp)
+		return -ENOMEM;
+
+	resolv->sess_map = cp;
+	stack = realloc(resolv->stack.arr, sizeof(*stack) * newcap);
+	if (!stack)
+		return -ENOMEM;
+	resolv->stack.arr = stack;
+
+	j = (uint16_t)newcap - 1;
+	for (idx = 0; j > resolv->sess_map_cap; idx++,j--)
+		stack[idx] = j;
+
+	resolv->sess_map_cap = newcap;
+
+	return 0;
+}
+
+static inline int pop_txid(struct dns_resolver *resolv)
+{
+	int r;
+	if (resolv->stack.top == 0) {
+		r = realloc_stack(resolv);
+		if (r)
+			return -EAGAIN;
+	}
+
+	return resolv->stack.arr[--resolv->stack.top];
+}
+
+static struct gwp_dns_entry *generate_gde(struct gwp_wrk *w, struct gwp_conn_pair *gcp, const char *host, const char *port)
+{
+	struct gwp_dns_entry *gde;
+	struct gwp_dns_ctx *dns = w->ctx->dns;
+	int txid;
+
+	txid = pop_txid(&w->dns_resolver);
+	if (txid < 0)
+		return  NULL;
+
+	gde = gwp_raw_dns_queue((uint16_t)txid, dns, host, port);
+	if (!gde) {
+		return_txid(w, gde);
+		return  NULL;
+	}
+	pr_dbg(&w->ctx->lh, "constructing standard DNS query packet for %s", host);
+	w->dns_resolver.sess_map[txid] = gcp;
+
+	return gde;
+}
+
+static int gwp_ctx_init_dns_resolver(struct gwp_wrk *w)
+{
+	struct dns_resolver *resolv;
+	struct gwp_dns_ctx *dns;
+	struct gwp_cfg *cfg;
+	struct gwp_ctx *ctx;
+	int udp_fd, r;
+	void *ptr;
+
+	ctx = w->ctx;
+	cfg = &ctx->cfg;
+	dns = ctx->dns;
+	udp_fd = __sys_socket(dns->ns_addr.sa.sa_family, SOCK_DGRAM | SOCK_NONBLOCK, 0);
+	if (udp_fd < 0) {
+		pr_err(&w->ctx->lh, "Failed to create udp_fd: %s\n", strerror(-udp_fd));
+		return udp_fd;
+	}
+
+	resolv = &w->dns_resolver;
+	ptr = calloc(cfg->sess_map_cap, sizeof(*resolv->stack.arr));
+	if (!ptr) {
+		r = -ENOMEM;
+		goto exit_close;
+	}
+	resolv->stack.arr = ptr;
+	ptr = calloc(cfg->sess_map_cap, sizeof(ptr));
+	if (!ptr) {
+		r = -ENOMEM;
+		goto exit_free;
+	}
+	resolv->sess_map = ptr;
+
+	resolv->udp_fd = udp_fd;
+	resolv->sess_map_cap = cfg->sess_map_cap;
+
+	r = cfg->sess_map_cap;
+	resolv->stack.top = r--;
+	for (; r <= 0; r--)
+		resolv->stack.arr[r] = r;
+
+	return 0;
+exit_free:
+	free(resolv->stack.arr);
+exit_close:
+	__sys_close(udp_fd);
+	return r;
+}
+
+static void gwp_ctx_free_raw_dns(struct gwp_wrk *w)
+{
+	struct dns_resolver *resolv = &w->dns_resolver;
+	__sys_close(resolv->udp_fd);
+	free(resolv->sess_map);
+	free(resolv->stack.arr);
+}
+#else
+static struct gwp_dns_entry *generate_gde(__maybe_unused struct gwp_wrk *w,
+					  __maybe_unused struct gwp_conn_pair *gcp,
+					  __maybe_unused const char *host,
+					  __maybe_unused const char *port)
+{
+	return NULL;
+}
+static int gwp_ctx_init_dns_resolver(__maybe_unused struct gwp_wrk *w)
+{
+	return 0;
+}
+
+static void gwp_ctx_free_raw_dns(__maybe_unused struct gwp_wrk *w)
+{
+}
+#endif /* #ifdef CONFIG_RAW_DNS */
+
 __cold
 static int gwp_ctx_init_thread(struct gwp_wrk *w,
 			       const struct gwp_sockaddr *bind_addr)
@@ -459,6 +615,14 @@ static int gwp_ctx_init_thread(struct gwp_wrk *w,
 		return r;
 	}
 
+	if (ctx->cfg.use_raw_dns) {
+		r = gwp_ctx_init_dns_resolver(w);
+		if (r) {
+			pr_err(&ctx->lh, "gwp_ctx_init_thread_event: %s\n", strerror(-r));
+			gwp_ctx_free_thread_sock(w);
+		}
+	}
+
 	r = gwp_ctx_init_thread_event(w);
 	if (r < 0) {
 		pr_err(&ctx->lh, "gwp_ctx_init_thread_event: %s\n", strerror(-r));
@@ -529,6 +693,8 @@ static void gwp_ctx_signal_all_workers(struct gwp_ctx *ctx)
 __cold
 static void gwp_ctx_free_thread(struct gwp_wrk *w)
 {
+	if (w->ctx->cfg.use_raw_dns)
+		gwp_ctx_free_raw_dns(w);
 	gwp_ctx_free_thread_sock_pairs(w);
 	gwp_ctx_free_thread_sock(w);
 	gwp_ctx_free_thread_event(w);
@@ -689,12 +855,23 @@ static int gwp_ctx_init_dns(struct gwp_ctx *ctx)
 {
 	struct gwp_cfg *cfg = &ctx->cfg;
 	const struct gwp_dns_cfg dns_cfg = {
+		.use_raw_dns = cfg->use_raw_dns,
 		.cache_expiry = cfg->socks5_dns_cache_secs,
 		.restyp = cfg->socks5_prefer_ipv6 ? GWP_DNS_RESTYP_PREFER_IPV6 : 0,
-		.nr_workers = cfg->nr_dns_workers
+		.nr_workers = 1,
+#ifdef CONFIG_RAW_DNS
+		.ns_addr_str = cfg->ns_addr_str
+#endif
 	};
 	int r;
 
+	if (cfg->use_raw_dns) {
+#ifndef CONFIG_RAW_DNS
+		pr_err(&ctx->lh, "raw DNS backend is not enabled in this build");
+		return -ENOSYS;
+#endif
+	}
+
 	if (!cfg->as_socks5 && !cfg->as_http) {
 		ctx->dns = NULL;
 		return 0;
@@ -733,6 +910,10 @@ static int gwp_ctx_parse_ev(struct gwp_ctx *ctx)
 		ctx->ev_used = GWP_EV_EPOLL;
 		pr_dbg(&ctx->lh, "Using event loop: epoll");
 	} else if (!strcmp(ev, "io_uring") || !strcmp(ev, "iou")) {
+		if (ctx->cfg.use_raw_dns) {
+			pr_err(&ctx->lh, "raw DNS backend is not supported for io_uring yet");
+			return -ENOSYS;
+		}
 		ctx->ev_used = GWP_EV_IO_URING;
 		pr_dbg(&ctx->lh, "Using event loop: io_uring");
 	} else {
@@ -990,8 +1171,15 @@ int gwp_free_conn_pair(struct gwp_wrk *w, struct gwp_conn_pair *gcp)
 	if (gcp->timer_fd >= 0)
 		__sys_close(gcp->timer_fd);
 
-	if (gcp->gde)
-		gwp_dns_entry_put(gcp->gde);
+	if (gcp->gde) {
+		if (w->ctx->cfg.use_raw_dns) {
+			pr_dbg(&w->ctx->lh, "client disconnected before query for %s resolved", gcp->gde->name);
+			return_txid(w, gcp->gde);
+			gwp_dns_raw_entry_free(w->ctx->dns, gcp->gde);
+		} else {
+			gwp_dns_entry_put(gcp->gde);
+		}
+	}
 
 	switch (gcp->prot_type) {
 	case GWP_PROT_TYPE_SOCKS5:
@@ -1192,7 +1380,12 @@ static int queue_dns_resolution(struct gwp_wrk *w, struct gwp_conn_pair *gcp,
 	struct gwp_dns_ctx *dns = w->ctx->dns;
 	struct gwp_dns_entry *gde;
 
-	gde = gwp_dns_queue(dns, host, port);
+	if (w->ctx->cfg.use_raw_dns) {
+		gde = generate_gde(w, gcp, host, port);
+	} else {
+		gde = gwp_dns_queue(dns, host, port);
+	}
+
 	if (unlikely(!gde)) {
 		pr_err(&w->ctx->lh, "Failed to allocate DNS entry for %s:%s", host, port);
 		return -ENOMEM;
diff --git a/src/gwproxy/gwproxy.h b/src/gwproxy/gwproxy.h
index 095c0ce700c3..31d8b56ad80f 100644
--- a/src/gwproxy/gwproxy.h
+++ b/src/gwproxy/gwproxy.h
@@ -9,6 +9,7 @@
 #define _GNU_SOURCE
 #endif
 
+#include <gwproxy/common.h>
 #include <gwproxy/syscall.h>
 #include <gwproxy/socks5.h>
 #include <gwproxy/dns.h>
@@ -24,6 +25,7 @@ struct gwp_cfg {
 	const char	*event_loop;
 	const char	*bind;
 	const char	*target;
+	bool		use_raw_dns;
 	bool		as_socks5;
 	bool		as_http;
 	bool		socks5_prefer_ipv6;
@@ -31,7 +33,6 @@ struct gwp_cfg {
 	const char	*socks5_auth_file;
 	int		socks5_dns_cache_secs;
 	int		nr_workers;
-	int		nr_dns_workers;
 	int		connect_timeout;
 	int		target_buf_size;
 	int		client_buf_size;
@@ -44,6 +45,10 @@ struct gwp_cfg {
 	int		log_level;
 	const char	*log_file;
 	const char	*pid_file;
+#ifdef CONFIG_RAW_DNS
+	const char	*ns_addr_str;
+	int		sess_map_cap;
+#endif
 };
 
 struct gwp_ctx;
@@ -195,8 +200,24 @@ struct iou {
 };
 #endif
 
+struct stack_u16 {
+	uint16_t top;
+	uint16_t *arr;
+};
+
+struct dns_resolver {
+	struct stack_u16	stack;
+	struct gwp_conn_pair	**sess_map;
+	uint16_t		sess_map_cap;
+	int			udp_fd;
+};
+
 struct gwp_wrk {
 	int			tcp_fd;
+#ifdef CONFIG_RAW_DNS
+	/* For mapping DNS queries to the corresponding proxy session */
+	struct dns_resolver	dns_resolver;
+#endif
 	struct gwp_conn_slot	conn_slot;
 
 	union {
@@ -251,6 +272,72 @@ int gwp_create_timer(int fd, int sec, int nsec);
 void gwp_setup_cli_sock_options(struct gwp_wrk *w, int fd);
 const char *ip_to_str(const struct gwp_sockaddr *gs);
 
+#ifdef CONFIG_RAW_DNS
+static inline void shrink_stack(struct gwp_wrk *w)
+{
+	struct dns_resolver *resolv;
+	struct gwp_cfg *cfg;
+	uint16_t *p1;
+	void *p2;
+	int i;
+
+	cfg = &w->ctx->cfg;
+	resolv = &w->dns_resolver;
+	p1 = realloc(resolv->stack.arr, cfg->sess_map_cap * sizeof(*resolv->stack.arr));
+	if (!p1)
+		return;
+	resolv->stack.arr = p1;
+
+	p2 = realloc(resolv->sess_map, cfg->sess_map_cap * sizeof(*resolv->sess_map));
+	if (!p2)
+		return;
+	resolv->sess_map = p2;
+
+	memset(p2, 0, cfg->sess_map_cap * sizeof(*resolv->sess_map));
+
+	i = cfg->sess_map_cap;
+	resolv->stack.top = i--;
+	for (; i <= 0; i--)
+		p1[i] = i;
+
+	resolv->sess_map_cap = cfg->sess_map_cap;
+}
+
+static inline int return_txid(struct gwp_wrk *w, struct gwp_dns_entry *gde)
+{
+	struct dns_resolver *resolv;
+	struct gwp_cfg *cfg;
+	uint16_t idx;
+
+	resolv = &w->dns_resolver;
+	cfg = &w->ctx->cfg;
+	/* the program shouldn't return more than its pop */
+	if (resolv->stack.top >= resolv->sess_map_cap) {
+		assert(0);
+		return -EINVAL;
+	}
+
+	idx = resolv->stack.top++;
+	/* shrinks it when there is no active query
+	 * and the allocated size is larger than the default size.
+	 */
+	if (idx == resolv->sess_map_cap &&
+		resolv->sess_map_cap > cfg->sess_map_cap) {
+		shrink_stack(w);
+	} else {
+		resolv->stack.arr[idx] = gde->txid;
+		resolv->sess_map[idx] = NULL;
+	}
+
+	return 0;
+}
+#else
+static inline int return_txid(__maybe_unused struct gwp_wrk *w, __maybe_unused struct gwp_dns_entry *gde)
+{
+	return 0;
+}
+#endif
+
 static inline void gwp_conn_buf_advance(struct gwp_conn *conn, size_t len)
 {
 	assert(len <= conn->len);
-- 
Ahmad Gani


^ permalink raw reply related	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-10 10:43 ` [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend Ahmad Gani
@ 2025-09-11 10:11   ` Alviro Iskandar Setiawan
  2025-09-12 10:37     ` Ahmad Gani
  2025-09-12 17:36     ` Ammar Faizi
  0 siblings, 2 replies; 13+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-09-11 10:11 UTC (permalink / raw)
  To: Ahmad Gani; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Wed, Sep 10, 2025 at 5:44 PM Ahmad Gani wrote:
> +       cfg = &w->ctx->cfg;
> +       resolv = &w->dns_resolver;
> +       p1 = realloc(resolv->stack.arr, cfg->sess_map_cap * sizeof(*resolv->stack.arr));
> +       if (!p1)
> +               return;
> +       resolv->stack.arr = p1;
> +
> +       p2 = realloc(resolv->sess_map, cfg->sess_map_cap * sizeof(*resolv->sess_map));
> +       if (!p2)
> +               return;
> +       resolv->sess_map = p2;

This shrink operation looks very *dangerous* to me. Think, what
happens if "p2 = realloc()" fails?

You update "->stack.arr = p1;" but given "->stack.top >=
->sess_map_cap", now ->stack.top is pointing to an index beyond the
array capacity because you have just successfully shrunk the array
capacity down to ->sess_map_cap. Next time, a client with SOCKS5
hostname comes in, you call pop_txid(), and you will explode
   ->stack.arr[--resolv->stack.top] ** OVERFLOW **
because ->stack.top is not updated. That kind of pattern is only safe
for array expansion, not for this shrink op definitely. You're allowed
to over allocate the size, but not under allocate.

> +       memset(p2, 0, cfg->sess_map_cap * sizeof(*resolv->sess_map));
> +
> +       i = cfg->sess_map_cap;
> +       resolv->stack.top = i--;
> +       for (; i <= 0; i--)
> +               p1[i] = i;
> +
> +       resolv->sess_map_cap = cfg->sess_map_cap;

Meanwhile, ->stack.top *is* only updated when p2 is successfully
shrunk, what if *only the p1 succeeds*?

My hunch is that you may still need some basic training on C
programming. Today, I am still not confident with your overall series.
This feature is still far away from an acceptable state. For now, big
NAK from me on this series.

Anyway, this patch is doing too many things. It's not something you
should do in a single commit. Please split it into smaller, more
manageable pieces.

tq
-- 
Software Engineer & ITPM Officer
Alviro Iskandar Setiawan
+1 908 777 0074

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-11 10:11   ` Alviro Iskandar Setiawan
@ 2025-09-12 10:37     ` Ahmad Gani
  2025-09-12 14:39       ` Alviro Iskandar Setiawan
  2025-09-12 17:36     ` Ammar Faizi
  1 sibling, 1 reply; 13+ messages in thread
From: Ahmad Gani @ 2025-09-12 10:37 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Thu, Sep 11, 2025 at 5:11 PM Alviro Iskandar Setiawan wrote:
> On Wed, Sep 10, 2025 at 5:44 PM Ahmad Gani wrote:
> > +       cfg = &w->ctx->cfg;
> > +       resolv = &w->dns_resolver;
> > +       p1 = realloc(resolv->stack.arr, cfg->sess_map_cap * sizeof(*resolv->stack.arr));
> > +       if (!p1)
> > +               return;
> > +       resolv->stack.arr = p1;
> > +
> > +       p2 = realloc(resolv->sess_map, cfg->sess_map_cap * sizeof(*resolv->sess_map));
> > +       if (!p2)
> > +               return;
> > +       resolv->sess_map = p2;
>
> This shrink operation looks very *dangerous* to me. Think, what
> happens if "p2 = realloc()" fails?
>
> You update "->stack.arr = p1;" but given "->stack.top >=
> ->sess_map_cap", now ->stack.top is pointing to an index beyond the
> array capacity because you have just successfully shrunk the array
> capacity down to ->sess_map_cap. Next time, a client with SOCKS5
> hostname comes in, you call pop_txid(), and you will explode
>    ->stack.arr[--resolv->stack.top] ** OVERFLOW **
> because ->stack.top is not updated. That kind of pattern is only safe
> for array expansion, not for this shrink op definitely. You're allowed
> to over allocate the size, but not under allocate.

That does sound dangerous. Even when shrinking, realloc can still fail if
it can't allocate a smaller block.

> > +       memset(p2, 0, cfg->sess_map_cap * sizeof(*resolv->sess_map));
> > +
> > +       i = cfg->sess_map_cap;
> > +       resolv->stack.top = i--;
> > +       for (; i <= 0; i--)
> > +               p1[i] = i;
> > +
> > +       resolv->sess_map_cap = cfg->sess_map_cap;
>
> Meanwhile, ->stack.top *is* only updated when p2 is successfully
> shrunk, what if *only the p1 succeeds*?

In this case, would it be better to allocate both sess_map and stack.arr
in a single block, so we only call realloc once?

> My hunch is that you may still need some basic training on C
> programming.

I'd also be happy to to go through a basic training session on C too! :3

> Anyway, this patch is doing too many things. It's not something you
> should do in a single commit. Please split it into smaller, more
> manageable pieces.

Alright, I will try to split the patches.

--
Ahmad Gani

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 10:37     ` Ahmad Gani
@ 2025-09-12 14:39       ` Alviro Iskandar Setiawan
  2025-09-12 15:49         ` Ammar Faizi
  2025-09-12 15:52         ` Ahmad Gani
  0 siblings, 2 replies; 13+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-09-12 14:39 UTC (permalink / raw)
  To: Ahmad Gani; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Fri, Sep 12, 2025 at 5:37 PM Ahmad Gani wrote:
> In this case, would it be better to allocate both sess_map and stack.arr
> in a single block, so we only call realloc once?

Why would it be better?

I don't think it's worth adding more complexity around it just to get
a non noticeable performance gain, unless you have the numbers?

It'll complicate the reallocation much, as you have to memmove()
around the values yourself. In fact, realloc() may return the same
pointer if it internally doesn't need to expand, or may even use
mremap() in case the memory was directly allocated via mmap() to
expand it without the need to call memmove().

-- 
Software Engineer & ITPM Officer
Alviro Iskandar Setiawan
+1 908 777 0074

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 14:39       ` Alviro Iskandar Setiawan
@ 2025-09-12 15:49         ` Ammar Faizi
  2025-09-12 15:52         ` Ahmad Gani
  1 sibling, 0 replies; 13+ messages in thread
From: Ammar Faizi @ 2025-09-12 15:49 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ahmad Gani, GNU/Weeb Mailing List

On Fri, Sep 12, 2025 at 09:39:36PM +0700, Alviro Iskandar Setiawan wrote:
> It'll complicate the reallocation much, as you have to memmove()
> around the values yourself. In fact, realloc() may return the same
> pointer if it internally doesn't need to expand, or may even use
> mremap() in case the memory was directly allocated via mmap() to
> expand it without the need to call memmove().

Ditto. Not only the memmove() will complicate thing, but also the size
and alignment differences will do too. Note that ->sess_map and
->stack.arr are not the same type.

-- 
Ammar Faizi


^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 14:39       ` Alviro Iskandar Setiawan
  2025-09-12 15:49         ` Ammar Faizi
@ 2025-09-12 15:52         ` Ahmad Gani
  2025-09-12 16:42           ` Alviro Iskandar Setiawan
  1 sibling, 1 reply; 13+ messages in thread
From: Ahmad Gani @ 2025-09-12 15:52 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Fri, Sep 12, 2025 at 9:39 PM Alviro Iskandar Setiawan wrote:
> On Fri, Sep 12, 2025 at 5:37 PM Ahmad Gani wrote:
> > In this case, would it be better to allocate both sess_map and stack.arr
> > in a single block, so we only call realloc once?
>
> Why would it be better?
>
> I don't think it's worth adding more complexity around it just to get
> a non noticeable performance gain, unless you have the numbers?
>
> It'll complicate the reallocation much, as you have to memmove()
> around the values yourself. In fact, realloc() may return the same
> pointer if it internally doesn't need to expand, or may even use
> mremap() in case the memory was directly allocated via mmap() to
> expand it without the need to call memmove().

My initial thought is not about performance, but because it can eliminate
the previous problem of two realloc when either realloc fails.

but after a second thought, I think I can just update the stack.top
instead of defer it.

Does this diff look ok?
https://github.com/realyukii/gwproxy/commit/352c80e00c53dd8e1459745a2e456d93ee3c8af0

--
Ahmad Gani

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 15:52         ` Ahmad Gani
@ 2025-09-12 16:42           ` Alviro Iskandar Setiawan
  2025-09-12 17:00             ` Alviro Iskandar Setiawan
  0 siblings, 1 reply; 13+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-09-12 16:42 UTC (permalink / raw)
  To: Ahmad Gani; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Fri, Sep 12, 2025 at 10:53 PM Ahmad Gani wrote:
> Does this diff look ok?
> https://github.com/realyukii/gwproxy/commit/352c80e00c53dd8e1459745a2e456d93ee3c8af0

OK, that looks better. Additionally, your reset_stack() function can
also be used at initialization. Note that realloc(NULL, 100) is the
same as calling malloc(100).

-- 
Software Engineer & ITPM Officer
Alviro Iskandar Setiawan
+1 908 777 0074

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 16:42           ` Alviro Iskandar Setiawan
@ 2025-09-12 17:00             ` Alviro Iskandar Setiawan
  2025-09-14  4:31               ` Ahmad Gani
  0 siblings, 1 reply; 13+ messages in thread
From: Alviro Iskandar Setiawan @ 2025-09-12 17:00 UTC (permalink / raw)
  To: Ahmad Gani; +Cc: Ammar Faizi, GNU/Weeb Mailing List

Please also reduce the use of #if{n,}def CONFIG_RAW_DNS, don't use it
all over the place. It's horrible to have them everywhere. Very
cluttering.

-- 
Software Engineer & ITPM Officer
Alviro Iskandar Setiawan
+1 908 777 0074

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-11 10:11   ` Alviro Iskandar Setiawan
  2025-09-12 10:37     ` Ahmad Gani
@ 2025-09-12 17:36     ` Ammar Faizi
  2025-09-14  4:34       ` Ahmad Gani
  1 sibling, 1 reply; 13+ messages in thread
From: Ammar Faizi @ 2025-09-12 17:36 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ahmad Gani, GNU/Weeb Mailing List

On Thu, Sep 11, 2025 at 05:11:11PM +0700, Alviro Iskandar Setiawan wrote:
> Anyway, this patch is doing too many things. It's not something you
> should do in a single commit. Please split it into smaller, more
> manageable pieces.

Indeed. This patch does too many things at once.

@Ahmad Gani, each patch should only do one substantial thing. Your patch
definitely needs to be split into smaller patches. If you want to
refactor something, do it indenpendently with your feature addiation.

For example:
  Patch 1: Export 'struct gwp_dns_ctx'.
  Patch 2: Add DNS parser code.
  Patch 3: Add stack code.
  Patch 4: Refactor something blablabla...
  Patch 5: Move it to that in order to blablabla...
  Patch 6: Integrate raw DNS feature to epoll.
  Patch 7: Integrate raw DNS feature to io_uring.
  Patch 8: Add --session-map-cap option.
  and so on...

This series is also a good example of splitting patches:
https://lore.kernel.org/io-uring/20240921080307.185186-1-axboe@kernel.dk/

P.S. Don't follow the example as is, adjust it with your own situation.
I don't want you arguing: "You gave me that example, so I did exactly
what you told me, even if it's not suitable for the situation in
question.".

-- 
Ammar Faizi


^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 17:00             ` Alviro Iskandar Setiawan
@ 2025-09-14  4:31               ` Ahmad Gani
  0 siblings, 0 replies; 13+ messages in thread
From: Ahmad Gani @ 2025-09-14  4:31 UTC (permalink / raw)
  To: Alviro Iskandar Setiawan; +Cc: Ammar Faizi, GNU/Weeb Mailing List

On Sat, Sep 13, 2025 at 12:00 AM Alviro Iskandar Setiawan wrote:
> Please also reduce the use of #if{n,}def CONFIG_RAW_DNS, don't use it
> all over the place. It's horrible to have them everywhere. Very
> cluttering.

I thought I had already reduced it. I can remove the #ifdef in the struct
definition if it's still considered cluttering.

--
Ahmad Gani

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend
  2025-09-12 17:36     ` Ammar Faizi
@ 2025-09-14  4:34       ` Ahmad Gani
  0 siblings, 0 replies; 13+ messages in thread
From: Ahmad Gani @ 2025-09-14  4:34 UTC (permalink / raw)
  To: Ammar Faizi; +Cc: Alviro Iskandar Setiawan, GNU/Weeb Mailing List

[-- Attachment #1: Type: text/plain, Size: 1408 bytes --]

On Sat, Sep 13, 2025 at 12:36 AM Ammar Faizi wrote:
> On Thu, Sep 11, 2025 at 05:11:11PM +0700, Alviro Iskandar Setiawan wrote:
> > Anyway, this patch is doing too many things. It's not something you
> > should do in a single commit. Please split it into smaller, more
> > manageable pieces.
>
> Indeed. This patch does too many things at once.
>
> @Ahmad Gani, each patch should only do one substantial thing. Your patch
> definitely needs to be split into smaller patches. If you want to
> refactor something, do it indenpendently with your feature addiation.
>
> For example:
>   Patch 1: Export 'struct gwp_dns_ctx'.
>   Patch 2: Add DNS parser code.
>   Patch 3: Add stack code.
>   Patch 4: Refactor something blablabla...
>   Patch 5: Move it to that in order to blablabla...
>   Patch 6: Integrate raw DNS feature to epoll.
>   Patch 7: Integrate raw DNS feature to io_uring.
>   Patch 8: Add --session-map-cap option.
>   and so on...
>
> This series is also a good example of splitting patches:
> https://lore.kernel.org/io-uring/20240921080307.185186-1-axboe@kernel.dk/
>
> P.S. Don't follow the example as is, adjust it with your own situation.
> I don't want you arguing: "You gave me that example, so I did exactly
> what you told me, even if it's not suitable for the situation in
> question.".

Understood, I'll revise and split the patch.

--
Ahmad Gani

[-- Attachment #2: Type: text/html, Size: 1753 bytes --]

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2025-09-14  4:35 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-09-10 10:43 [PATCH gwproxy v10 0/2] Initial work on integration of DNS parser lib in gwproxy Ahmad Gani
2025-09-10 10:43 ` [PATCH gwproxy v10 1/2] dnsparser: Add dns parser code Ahmad Gani
2025-09-10 10:43 ` [PATCH gwproxy v10 2/2] gwproxy: refactor code base to add experimental raw DNS backend Ahmad Gani
2025-09-11 10:11   ` Alviro Iskandar Setiawan
2025-09-12 10:37     ` Ahmad Gani
2025-09-12 14:39       ` Alviro Iskandar Setiawan
2025-09-12 15:49         ` Ammar Faizi
2025-09-12 15:52         ` Ahmad Gani
2025-09-12 16:42           ` Alviro Iskandar Setiawan
2025-09-12 17:00             ` Alviro Iskandar Setiawan
2025-09-14  4:31               ` Ahmad Gani
2025-09-12 17:36     ` Ammar Faizi
2025-09-14  4:34       ` Ahmad Gani

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox