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