* [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