mirror of
https://github.com/Telecominfraproject/wlan-ap.git
synced 2025-10-29 17:42:41 +00:00
1134 lines
26 KiB
Diff
1134 lines
26 KiB
Diff
From c9e68bb8f6830b053537b2fc62b78172bd815ff2 Mon Sep 17 00:00:00 2001
|
|
From: Jo-Philipp Wich <jo@mein.io>
|
|
Date: Thu, 21 Oct 2021 00:18:55 +0200
|
|
Subject: [PATCH] lib: introduce resolver library
|
|
|
|
This adds a simple, UDP-only DNS resolver library mimicking the operation of
|
|
the extended busybox nslookup applet.
|
|
|
|
Simply querying a domain name will perform A + AAAA resolving by default:
|
|
|
|
# ucode -mresolv -Rs 'printf("%.J\n", resolv.query("example.com"))'
|
|
{
|
|
"example.com": {
|
|
"A": [
|
|
"93.184.216.34"
|
|
],
|
|
"AAAA": [
|
|
"2606:2800:220:1:248:1893:25c8:1946"
|
|
]
|
|
}
|
|
}
|
|
|
|
Passing IP addresses will automatically perform PTR requests:
|
|
|
|
# ucode -mresolv -Rs 'printf("%.J\n", resolv.query("8.8.8.8"))'
|
|
{
|
|
"8.8.8.8.in-addr.arpa": {
|
|
"PTR": [
|
|
"dns.google"
|
|
]
|
|
}
|
|
}
|
|
|
|
# ucode -mresolv -Rs 'printf("%.J\n", resolv.query("2001:4860:4860::8888"))'
|
|
{
|
|
"8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.ip6.arpa": {
|
|
"PTR": [
|
|
"dns.google"
|
|
]
|
|
}
|
|
}
|
|
|
|
Additional options for query type and nameserver selection can be passed via
|
|
a second optional options dictionary:
|
|
|
|
# ucode -mresolv -Rs 'printf("%.J\n",
|
|
resolv.query([ "openwrt.org", "example.org", "doesnotexist.tld" ], {
|
|
type: [ "A", "AAAA", "MX" ],
|
|
nameserver: [ "1.1.1.1", "8.8.4.4" ],
|
|
timeout: 5000,
|
|
retries: 2,
|
|
edns_maxsize: 4096
|
|
}))'
|
|
{
|
|
"openwrt.org": {
|
|
"A": [
|
|
"139.59.209.225"
|
|
],
|
|
"MX": [
|
|
[
|
|
10,
|
|
"util-01.infra.openwrt.org"
|
|
]
|
|
],
|
|
"AAAA": [
|
|
"2a03:b0c0:3:d0::1af1:1"
|
|
]
|
|
},
|
|
"example.org": {
|
|
"A": [
|
|
"93.184.216.34"
|
|
],
|
|
"AAAA": [
|
|
"2606:2800:220:1:248:1893:25c8:1946"
|
|
],
|
|
"MX": [
|
|
[
|
|
0,
|
|
"."
|
|
]
|
|
]
|
|
},
|
|
"doesnotexist.tld": {
|
|
"rcode": "NXDOMAIN"
|
|
}
|
|
}
|
|
|
|
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
|
|
---
|
|
CMakeLists.txt | 15 +
|
|
lib/resolv.c | 1000 ++++++++++++++++++++++++++++++++++++++++++++++++
|
|
2 files changed, 1015 insertions(+)
|
|
create mode 100644 lib/resolv.c
|
|
|
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index f993019..e5fb58a 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -19,6 +19,7 @@ OPTION(UBUS_SUPPORT "Ubus plugin support" ON)
|
|
OPTION(UCI_SUPPORT "UCI plugin support" ON)
|
|
OPTION(RTNL_SUPPORT "Route Netlink plugin support" ON)
|
|
OPTION(NL80211_SUPPORT "Wireless Netlink plugin support" ON)
|
|
+OPTION(RESOLV_SUPPORT "NS resolve plugin support" ON)
|
|
|
|
OPTION(LEGACY_SUPPORT "Support deprecated syntax features" ON)
|
|
|
|
@@ -136,6 +137,20 @@ IF(NL80211_SUPPORT)
|
|
TARGET_LINK_LIBRARIES(nl80211_lib ${nl})
|
|
ENDIF()
|
|
|
|
+IF(RESOLV_SUPPORT)
|
|
+ SET(LIBRARIES ${LIBRARIES} resolv_lib)
|
|
+ ADD_LIBRARY(resolv_lib MODULE lib/resolv.c)
|
|
+ SET_TARGET_PROPERTIES(resolv_lib PROPERTIES OUTPUT_NAME resolv PREFIX "")
|
|
+ CHECK_FUNCTION_EXISTS(res_mkquery RES_MKQUERY_FUNCTION_EXISTS)
|
|
+ CHECK_FUNCTION_EXISTS(clock_gettime CLOCK_GETTIME_FUNCTION_EXISTS)
|
|
+ IF (NOT RES_MKQUERY_FUNCTION_EXISTS)
|
|
+ TARGET_LINK_LIBRARIES(resolv_lib resolv)
|
|
+ ENDIF()
|
|
+ IF (NOT CLOCK_GETTIME_FUNCTION_EXISTS)
|
|
+ TARGET_LINK_LIBRARIES(resolv_lib rt)
|
|
+ ENDIF()
|
|
+ENDIF()
|
|
+
|
|
IF(UNIT_TESTING)
|
|
ENABLE_TESTING()
|
|
ADD_DEFINITIONS(-DUNIT_TESTING)
|
|
diff --git a/lib/resolv.c b/lib/resolv.c
|
|
new file mode 100644
|
|
index 0000000..fe704fa
|
|
--- /dev/null
|
|
+++ b/lib/resolv.c
|
|
@@ -0,0 +1,1000 @@
|
|
+/*
|
|
+ * nslookup_lede - musl compatible replacement for busybox nslookup
|
|
+ *
|
|
+ * Copyright (C) 2017 Jo-Philipp Wich <jo@mein.io>
|
|
+ *
|
|
+ * Permission to use, copy, modify, and/or distribute this software for any
|
|
+ * purpose with or without fee is hereby granted, provided that the above
|
|
+ * copyright notice and this permission notice appear in all copies.
|
|
+ *
|
|
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
+ */
|
|
+
|
|
+#include <stdio.h>
|
|
+#include <resolv.h>
|
|
+#include <string.h>
|
|
+#include <errno.h>
|
|
+#include <time.h>
|
|
+#include <poll.h>
|
|
+#include <unistd.h>
|
|
+#include <stdlib.h>
|
|
+#include <sys/socket.h>
|
|
+#include <arpa/inet.h>
|
|
+#include <net/if.h>
|
|
+#include <netdb.h>
|
|
+
|
|
+#include "ucode/module.h"
|
|
+
|
|
+#define for_each_item(arr, item) \
|
|
+ for (uc_value_t *_idx = NULL, *item = (ucv_type(arr) == UC_ARRAY) ? ucv_array_get(arr, 0) : arr; \
|
|
+ (uintptr_t)_idx < (ucv_type(arr) == UC_ARRAY ? ucv_array_length(arr) : (arr != NULL)); \
|
|
+ _idx = (void *)((uintptr_t)_idx + 1), item = ucv_array_get(arr, (uintptr_t)_idx))
|
|
+
|
|
+#define err_return(code, ...) do { set_error(code, __VA_ARGS__); return NULL; } while(0)
|
|
+
|
|
+static struct {
|
|
+ int code;
|
|
+ char *msg;
|
|
+} last_error;
|
|
+
|
|
+__attribute__((format(printf, 2, 3))) static void
|
|
+set_error(int errcode, const char *fmt, ...) {
|
|
+ va_list ap;
|
|
+
|
|
+ free(last_error.msg);
|
|
+
|
|
+ last_error.code = errcode;
|
|
+ last_error.msg = NULL;
|
|
+
|
|
+ if (fmt) {
|
|
+ va_start(ap, fmt);
|
|
+ vasprintf(&last_error.msg, fmt, ap);
|
|
+ va_end(ap);
|
|
+ }
|
|
+}
|
|
+
|
|
+typedef struct {
|
|
+ socklen_t len;
|
|
+ union {
|
|
+ struct sockaddr sa;
|
|
+ struct sockaddr_in sin;
|
|
+ struct sockaddr_in6 sin6;
|
|
+ } u;
|
|
+} addr_t;
|
|
+
|
|
+typedef struct {
|
|
+ const char *name;
|
|
+ addr_t addr;
|
|
+} ns_t;
|
|
+
|
|
+typedef struct {
|
|
+ char *name;
|
|
+ size_t qlen, rlen;
|
|
+ unsigned char query[512];
|
|
+ int rcode;
|
|
+} query_t;
|
|
+
|
|
+typedef struct __attribute__((packed)) {
|
|
+ uint8_t root_domain;
|
|
+ uint16_t type;
|
|
+ uint16_t edns_maxsize;
|
|
+ uint8_t extended_rcode;
|
|
+ uint8_t edns_version;
|
|
+ uint16_t z;
|
|
+ uint16_t data_length;
|
|
+} opt_rr_t;
|
|
+
|
|
+typedef struct {
|
|
+ uint32_t qtypes;
|
|
+ size_t n_ns;
|
|
+ ns_t *ns;
|
|
+ size_t n_queries;
|
|
+ query_t *queries;
|
|
+ uint32_t retries;
|
|
+ uint32_t timeout;
|
|
+ uint16_t edns_maxsize;
|
|
+} resolve_ctx_t;
|
|
+
|
|
+
|
|
+static struct {
|
|
+ int type;
|
|
+ const char *name;
|
|
+} qtypes[] = {
|
|
+ { ns_t_soa, "SOA" },
|
|
+ { ns_t_ns, "NS" },
|
|
+ { ns_t_a, "A" },
|
|
+ { ns_t_aaaa, "AAAA" },
|
|
+ { ns_t_cname, "CNAME" },
|
|
+ { ns_t_mx, "MX" },
|
|
+ { ns_t_txt, "TXT" },
|
|
+ { ns_t_srv, "SRV" },
|
|
+ { ns_t_ptr, "PTR" },
|
|
+ { ns_t_any, "ANY" },
|
|
+ { }
|
|
+};
|
|
+
|
|
+static const char *rcodes[] = {
|
|
+ "NOERROR",
|
|
+ "FORMERR",
|
|
+ "SERVFAIL",
|
|
+ "NXDOMAIN",
|
|
+ "NOTIMP",
|
|
+ "REFUSED",
|
|
+ "YXDOMAIN",
|
|
+ "YXRRSET",
|
|
+ "NXRRSET",
|
|
+ "NOTAUTH",
|
|
+ "NOTZONE",
|
|
+ "RESERVED11",
|
|
+ "RESERVED12",
|
|
+ "RESERVED13",
|
|
+ "RESERVED14",
|
|
+ "RESERVED15",
|
|
+ "BADVERS"
|
|
+};
|
|
+
|
|
+static unsigned int default_port = 53;
|
|
+
|
|
+
|
|
+static uc_value_t *
|
|
+init_obj(uc_vm_t *vm, uc_value_t *obj, const char *key, uc_type_t type)
|
|
+{
|
|
+ uc_value_t *existing;
|
|
+
|
|
+ existing = ucv_object_get(obj, key, NULL);
|
|
+
|
|
+ if (existing == NULL) {
|
|
+ switch (type) {
|
|
+ case UC_ARRAY:
|
|
+ existing = ucv_array_new(vm);
|
|
+ break;
|
|
+
|
|
+ case UC_OBJECT:
|
|
+ existing = ucv_object_new(vm);
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ ucv_object_add(obj, key, existing);
|
|
+ }
|
|
+
|
|
+ return existing;
|
|
+}
|
|
+
|
|
+static int
|
|
+parse_reply(uc_vm_t *vm, uc_value_t *res_obj, const unsigned char *msg, size_t len)
|
|
+{
|
|
+ ns_msg handle;
|
|
+ ns_rr rr;
|
|
+ int i, n, rdlen;
|
|
+ const char *key = NULL;
|
|
+ char astr[INET6_ADDRSTRLEN], dname[MAXDNAME];
|
|
+ const unsigned char *cp;
|
|
+ uc_value_t *name_obj, *type_arr, *item;
|
|
+
|
|
+ if (ns_initparse(msg, len, &handle) != 0) {
|
|
+ set_error(errno, "Unable to parse reply packet");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ for (i = 0; i < ns_msg_count(handle, ns_s_an); i++) {
|
|
+ if (ns_parserr(&handle, ns_s_an, i, &rr) != 0) {
|
|
+ set_error(errno, "Unable to parse resource record");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ name_obj = init_obj(vm, res_obj, ns_rr_name(rr), UC_OBJECT);
|
|
+
|
|
+ rdlen = ns_rr_rdlen(rr);
|
|
+
|
|
+ switch (ns_rr_type(rr))
|
|
+ {
|
|
+ case ns_t_a:
|
|
+ if (rdlen != 4) {
|
|
+ set_error(EBADMSG, "Invalid A record length");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, "A", UC_ARRAY);
|
|
+
|
|
+ inet_ntop(AF_INET, ns_rr_rdata(rr), astr, sizeof(astr));
|
|
+ ucv_array_push(type_arr, ucv_string_new(astr));
|
|
+ break;
|
|
+
|
|
+ case ns_t_aaaa:
|
|
+ if (rdlen != 16) {
|
|
+ set_error(EBADMSG, "Invalid AAAA record length");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, "AAAA", UC_ARRAY);
|
|
+
|
|
+ inet_ntop(AF_INET6, ns_rr_rdata(rr), astr, sizeof(astr));
|
|
+ ucv_array_push(type_arr, ucv_string_new(astr));
|
|
+ break;
|
|
+
|
|
+ case ns_t_ns:
|
|
+ if (!key)
|
|
+ key = "NS";
|
|
+ /* fall through */
|
|
+
|
|
+ case ns_t_cname:
|
|
+ if (!key)
|
|
+ key = "CNAME";
|
|
+ /* fall through */
|
|
+
|
|
+ case ns_t_ptr:
|
|
+ if (!key)
|
|
+ key = "PTR";
|
|
+
|
|
+ if (ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
|
|
+ ns_rr_rdata(rr), dname, sizeof(dname)) < 0) {
|
|
+ set_error(errno, "Unable to uncompress domain name");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, key, UC_ARRAY);
|
|
+ n = ucv_array_length(type_arr);
|
|
+ item = n ? ucv_array_get(type_arr, n - 1) : NULL;
|
|
+
|
|
+ if (!n || strcmp(ucv_string_get(item), dname))
|
|
+ ucv_array_push(type_arr, ucv_string_new(dname));
|
|
+
|
|
+ break;
|
|
+
|
|
+ case ns_t_mx:
|
|
+ if (rdlen < 2) {
|
|
+ set_error(EBADMSG, "MX record too short");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ n = ns_get16(ns_rr_rdata(rr));
|
|
+
|
|
+ if (ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
|
|
+ ns_rr_rdata(rr) + 2, dname, sizeof(dname)) < 0) {
|
|
+ set_error(errno, "Unable to uncompress MX domain");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, "MX", UC_ARRAY);
|
|
+ item = ucv_array_new_length(vm, 2);
|
|
+ ucv_array_push(item, ucv_int64_new(n));
|
|
+ ucv_array_push(item, ucv_string_new(dname));
|
|
+ ucv_array_push(type_arr, item);
|
|
+ break;
|
|
+
|
|
+ case ns_t_txt:
|
|
+ if (rdlen < 1) {
|
|
+ set_error(EBADMSG, "TXT record too short");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ n = *(unsigned char *)ns_rr_rdata(rr);
|
|
+
|
|
+ if (n > 0) {
|
|
+ memset(dname, 0, sizeof(dname));
|
|
+ memcpy(dname, ns_rr_rdata(rr) + 1, n);
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, "TXT", UC_ARRAY);
|
|
+ ucv_array_push(type_arr, ucv_string_new(dname));
|
|
+ }
|
|
+ break;
|
|
+
|
|
+ case ns_t_srv:
|
|
+ if (rdlen < 6) {
|
|
+ set_error(EBADMSG, "SRV record too short");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ cp = ns_rr_rdata(rr);
|
|
+ n = ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
|
|
+ cp + 6, dname, sizeof(dname));
|
|
+
|
|
+ if (n < 0) {
|
|
+ set_error(errno, "Unable to uncompress domain name");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, "SRV", UC_ARRAY);
|
|
+ item = ucv_array_new_length(vm, 4);
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get16(cp)));
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get16(cp + 2)));
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get16(cp + 4)));
|
|
+ ucv_array_push(item, ucv_string_new(dname));
|
|
+ ucv_array_push(type_arr, item);
|
|
+ break;
|
|
+
|
|
+ case ns_t_soa:
|
|
+ if (rdlen < 20) {
|
|
+ set_error(EBADMSG, "SOA record too short");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ type_arr = init_obj(vm, name_obj, "SOA", UC_ARRAY);
|
|
+ item = ucv_array_new_length(vm, 7);
|
|
+
|
|
+ cp = ns_rr_rdata(rr);
|
|
+ n = ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
|
|
+ cp, dname, sizeof(dname));
|
|
+
|
|
+ if (n < 0) {
|
|
+ set_error(errno, "Unable to uncompress domain name");
|
|
+ ucv_put(item);
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ ucv_array_push(item, ucv_string_new(dname)); /* origin */
|
|
+ cp += n;
|
|
+
|
|
+ n = ns_name_uncompress(ns_msg_base(handle), ns_msg_end(handle),
|
|
+ cp, dname, sizeof(dname));
|
|
+
|
|
+ if (n < 0) {
|
|
+ set_error(errno, "Unable to uncompress domain name");
|
|
+ ucv_put(item);
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ ucv_array_push(item, ucv_string_new(dname)); /* mail addr */
|
|
+ cp += n;
|
|
+
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* serial */
|
|
+ cp += 4;
|
|
+
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* refresh */
|
|
+ cp += 4;
|
|
+
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* retry */
|
|
+ cp += 4;
|
|
+
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* expire */
|
|
+ cp += 4;
|
|
+
|
|
+ ucv_array_push(item, ucv_int64_new(ns_get32(cp))); /* minimum */
|
|
+
|
|
+ ucv_array_push(type_arr, item);
|
|
+ break;
|
|
+
|
|
+ default:
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return i;
|
|
+}
|
|
+
|
|
+static int
|
|
+parse_nsaddr(const char *addrstr, addr_t *lsa)
|
|
+{
|
|
+ char *eptr, *hash, ifname[IFNAMSIZ];
|
|
+ unsigned int port = default_port;
|
|
+ unsigned int scope = 0;
|
|
+
|
|
+ hash = strchr(addrstr, '#');
|
|
+
|
|
+ if (hash) {
|
|
+ *hash++ = '\0';
|
|
+ port = strtoul(hash, &eptr, 10);
|
|
+
|
|
+ if (eptr == hash || *eptr != '\0' || port > 65535) {
|
|
+ errno = EINVAL;
|
|
+ return -1;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ hash = strchr(addrstr, '%');
|
|
+
|
|
+ if (hash) {
|
|
+ for (eptr = ++hash; *eptr != '\0' && *eptr != '#'; eptr++) {
|
|
+ if ((eptr - hash) >= IFNAMSIZ) {
|
|
+ errno = ENODEV;
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ ifname[eptr - hash] = *eptr;
|
|
+ }
|
|
+
|
|
+ ifname[eptr - hash] = '\0';
|
|
+ scope = if_nametoindex(ifname);
|
|
+
|
|
+ if (scope == 0) {
|
|
+ errno = ENODEV;
|
|
+ return -1;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (inet_pton(AF_INET6, addrstr, &lsa->u.sin6.sin6_addr)) {
|
|
+ lsa->u.sin6.sin6_family = AF_INET6;
|
|
+ lsa->u.sin6.sin6_port = htons(port);
|
|
+ lsa->u.sin6.sin6_scope_id = scope;
|
|
+ lsa->len = sizeof(lsa->u.sin6);
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ if (!scope && inet_pton(AF_INET, addrstr, &lsa->u.sin.sin_addr)) {
|
|
+ lsa->u.sin.sin_family = AF_INET;
|
|
+ lsa->u.sin.sin_port = htons(port);
|
|
+ lsa->len = sizeof(lsa->u.sin);
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ errno = EINVAL;
|
|
+ return -1;
|
|
+}
|
|
+
|
|
+static char *
|
|
+make_ptr(const char *addrstr)
|
|
+{
|
|
+ const char *hexdigit = "0123456789abcdef";
|
|
+ static char ptrstr[73];
|
|
+ unsigned char addr[16];
|
|
+ char *ptr = ptrstr;
|
|
+ int i;
|
|
+
|
|
+ if (inet_pton(AF_INET6, addrstr, addr)) {
|
|
+ if (memcmp(addr, "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12) != 0) {
|
|
+ for (i = 0; i < 16; i++) {
|
|
+ *ptr++ = hexdigit[(unsigned char)addr[15 - i] & 0xf];
|
|
+ *ptr++ = '.';
|
|
+ *ptr++ = hexdigit[(unsigned char)addr[15 - i] >> 4];
|
|
+ *ptr++ = '.';
|
|
+ }
|
|
+ strcpy(ptr, "ip6.arpa");
|
|
+ }
|
|
+ else {
|
|
+ sprintf(ptr, "%u.%u.%u.%u.in-addr.arpa",
|
|
+ addr[15], addr[14], addr[13], addr[12]);
|
|
+ }
|
|
+
|
|
+ return ptrstr;
|
|
+ }
|
|
+
|
|
+ if (inet_pton(AF_INET, addrstr, addr)) {
|
|
+ sprintf(ptr, "%u.%u.%u.%u.in-addr.arpa",
|
|
+ addr[3], addr[2], addr[1], addr[0]);
|
|
+ return ptrstr;
|
|
+ }
|
|
+
|
|
+ return NULL;
|
|
+}
|
|
+
|
|
+static unsigned long
|
|
+mtime(void)
|
|
+{
|
|
+ struct timespec ts;
|
|
+ clock_gettime(CLOCK_REALTIME, &ts);
|
|
+
|
|
+ return (unsigned long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
+}
|
|
+
|
|
+static void
|
|
+to_v4_mapped(addr_t *a)
|
|
+{
|
|
+ if (a->u.sa.sa_family != AF_INET)
|
|
+ return;
|
|
+
|
|
+ memcpy(a->u.sin6.sin6_addr.s6_addr + 12,
|
|
+ &a->u.sin.sin_addr, 4);
|
|
+
|
|
+ memcpy(a->u.sin6.sin6_addr.s6_addr,
|
|
+ "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12);
|
|
+
|
|
+ a->u.sin6.sin6_family = AF_INET6;
|
|
+ a->u.sin6.sin6_flowinfo = 0;
|
|
+ a->u.sin6.sin6_scope_id = 0;
|
|
+ a->len = sizeof(a->u.sin6);
|
|
+}
|
|
+
|
|
+static void
|
|
+add_status(uc_vm_t *vm, uc_value_t *res_obj, const char *name, const char *rcode)
|
|
+{
|
|
+ uc_value_t *name_obj = init_obj(vm, res_obj, name, UC_OBJECT);
|
|
+
|
|
+ ucv_object_add(name_obj, "rcode", ucv_string_new(rcode));
|
|
+}
|
|
+
|
|
+/*
|
|
+ * Function logic borrowed & modified from musl libc, res_msend.c
|
|
+ */
|
|
+
|
|
+static int
|
|
+send_queries(resolve_ctx_t *ctx, uc_vm_t *vm, uc_value_t *res_obj)
|
|
+{
|
|
+ int fd;
|
|
+ int servfail_retry = 0;
|
|
+ addr_t from = { };
|
|
+ int one = 1;
|
|
+ int recvlen = 0;
|
|
+ int n_replies = 0;
|
|
+ struct pollfd pfd;
|
|
+ unsigned long t0, t1, t2, timeout = ctx->timeout, retry_interval;
|
|
+ unsigned int nn, qn, next_query = 0;
|
|
+ struct { unsigned char *buf; size_t len; } reply_buf = { 0 };
|
|
+
|
|
+ from.u.sa.sa_family = AF_INET;
|
|
+ from.len = sizeof(from.u.sin);
|
|
+
|
|
+ for (nn = 0; nn < ctx->n_ns; nn++) {
|
|
+ if (ctx->ns[nn].addr.u.sa.sa_family == AF_INET6) {
|
|
+ from.u.sa.sa_family = AF_INET6;
|
|
+ from.len = sizeof(from.u.sin6);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* Get local address and open/bind a socket */
|
|
+ fd = socket(from.u.sa.sa_family, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
|
|
+
|
|
+ /* Handle case where system lacks IPv6 support */
|
|
+ if (fd < 0 && from.u.sa.sa_family == AF_INET6 && errno == EAFNOSUPPORT) {
|
|
+ fd = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
|
|
+ from.u.sa.sa_family = AF_INET;
|
|
+ }
|
|
+
|
|
+ if (fd < 0) {
|
|
+ set_error(errno, "Unable to open UDP socket");
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ if (bind(fd, &from.u.sa, from.len) < 0) {
|
|
+ set_error(errno, "Unable to bind UDP socket");
|
|
+ close(fd);
|
|
+
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ /* Convert any IPv4 addresses in a mixed environment to v4-mapped */
|
|
+ if (from.u.sa.sa_family == AF_INET6) {
|
|
+ setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one));
|
|
+
|
|
+ for (nn = 0; nn < ctx->n_ns; nn++)
|
|
+ to_v4_mapped(&ctx->ns[nn].addr);
|
|
+ }
|
|
+
|
|
+ pfd.fd = fd;
|
|
+ pfd.events = POLLIN;
|
|
+ retry_interval = timeout / ctx->retries;
|
|
+ t0 = t2 = mtime();
|
|
+ t1 = t2 - retry_interval;
|
|
+
|
|
+ for (; t2 - t0 < timeout; t2 = mtime()) {
|
|
+ if (t2 - t1 >= retry_interval) {
|
|
+ for (qn = 0; qn < ctx->n_queries; qn++) {
|
|
+ if (ctx->queries[qn].rcode == 0 || ctx->queries[qn].rcode == 3)
|
|
+ continue;
|
|
+
|
|
+ for (nn = 0; nn < ctx->n_ns; nn++) {
|
|
+ sendto(fd, ctx->queries[qn].query, ctx->queries[qn].qlen,
|
|
+ MSG_NOSIGNAL, &ctx->ns[nn].addr.u.sa, ctx->ns[nn].addr.len);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ t1 = t2;
|
|
+ servfail_retry = 2 * ctx->n_queries;
|
|
+ }
|
|
+
|
|
+ /* Wait for a response, or until time to retry */
|
|
+ switch (poll(&pfd, 1, t1+retry_interval-t2)) {
|
|
+ case 0:
|
|
+ /* timeout */
|
|
+ for (qn = 0; qn < ctx->n_queries; qn++) {
|
|
+ if (ctx->queries[qn].rcode != -1)
|
|
+ continue;
|
|
+
|
|
+ for (nn = 0; nn < ctx->n_ns; nn++)
|
|
+ add_status(vm, res_obj, ctx->queries[qn].name, "TIMEOUT");
|
|
+ }
|
|
+
|
|
+ continue;
|
|
+
|
|
+ case -1:
|
|
+ /* error */
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ while (1) {
|
|
+ recvlen = recvfrom(fd, NULL, 0, MSG_PEEK|MSG_TRUNC, &from.u.sa, &from.len);
|
|
+
|
|
+ /* read error */
|
|
+ if (recvlen < 0)
|
|
+ break;
|
|
+
|
|
+ if ((size_t)recvlen > reply_buf.len) {
|
|
+ reply_buf.buf = xrealloc(reply_buf.buf, recvlen);
|
|
+ reply_buf.len = recvlen;
|
|
+ }
|
|
+
|
|
+ recvlen = recvfrom(fd, reply_buf.buf, recvlen, 0, &from.u.sa, &from.len);
|
|
+
|
|
+ /* Ignore non-identifiable packets */
|
|
+ if (recvlen < 4)
|
|
+ continue;
|
|
+
|
|
+ /* Ignore replies from addresses we didn't send to */
|
|
+ for (nn = 0; nn < ctx->n_ns; nn++)
|
|
+ if (memcmp(&from.u.sa, &ctx->ns[nn].addr.u.sa, from.len) == 0)
|
|
+ break;
|
|
+
|
|
+ if (nn >= ctx->n_ns)
|
|
+ continue;
|
|
+
|
|
+ /* Find which query this answer goes with, if any */
|
|
+ for (qn = next_query; qn < ctx->n_queries; qn++)
|
|
+ if (!memcmp(reply_buf.buf, ctx->queries[qn].query, 2))
|
|
+ break;
|
|
+
|
|
+ /* Do not overwrite previous replies from other servers
|
|
+ * but allow overwriting preexisting NXDOMAIN reply */
|
|
+ if (qn >= ctx->n_queries ||
|
|
+ ctx->queries[qn].rcode == 0 ||
|
|
+ (ctx->queries[qn].rcode == 3 && (reply_buf.buf[3] & 15) != 0))
|
|
+ continue;
|
|
+
|
|
+ ctx->queries[qn].rcode = reply_buf.buf[3] & 15;
|
|
+
|
|
+ switch (ctx->queries[qn].rcode) {
|
|
+ case 0:
|
|
+ ucv_object_delete(
|
|
+ ucv_object_get(res_obj, ctx->queries[qn].name, NULL),
|
|
+ "rcodes");
|
|
+
|
|
+ break;
|
|
+
|
|
+ case 2:
|
|
+ /* Retry immediately on server failure. */
|
|
+ if (servfail_retry && servfail_retry--)
|
|
+ sendto(fd, ctx->queries[qn].query, ctx->queries[qn].qlen,
|
|
+ MSG_NOSIGNAL, &ctx->ns[nn].addr.u.sa, ctx->ns[nn].addr.len);
|
|
+
|
|
+ /* fall through */
|
|
+
|
|
+ default:
|
|
+ add_status(vm, res_obj, ctx->queries[qn].name,
|
|
+ rcodes[ctx->queries[qn].rcode]);
|
|
+ }
|
|
+
|
|
+ /* Store answer */
|
|
+ n_replies++;
|
|
+
|
|
+ ctx->queries[qn].rlen = recvlen;
|
|
+
|
|
+ parse_reply(vm, res_obj, reply_buf.buf, recvlen);
|
|
+
|
|
+ if (qn == next_query) {
|
|
+ while (next_query < ctx->n_queries) {
|
|
+ if (ctx->queries[next_query].rcode == -1)
|
|
+ break;
|
|
+
|
|
+ next_query++;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (next_query >= ctx->n_queries)
|
|
+ goto out;
|
|
+ }
|
|
+ }
|
|
+
|
|
+out:
|
|
+ free(reply_buf.buf);
|
|
+
|
|
+ return n_replies;
|
|
+}
|
|
+
|
|
+static ns_t *
|
|
+add_ns(resolve_ctx_t *ctx, const char *addr)
|
|
+{
|
|
+ char portstr[sizeof("65535")], *p;
|
|
+ addr_t a = { };
|
|
+ struct addrinfo *ai, *aip, hints = {
|
|
+ .ai_flags = AI_NUMERICSERV,
|
|
+ .ai_socktype = SOCK_DGRAM
|
|
+ };
|
|
+
|
|
+ if (parse_nsaddr(addr, &a)) {
|
|
+ /* Maybe we got a domain name, attempt to resolve it using the standard
|
|
+ * resolver routines */
|
|
+
|
|
+ p = strchr(addr, '#');
|
|
+ snprintf(portstr, sizeof(portstr), "%hu",
|
|
+ (unsigned short)(p ? strtoul(p, NULL, 10) : default_port));
|
|
+
|
|
+ if (!getaddrinfo(addr, portstr, &hints, &ai)) {
|
|
+ for (aip = ai; aip; aip = aip->ai_next) {
|
|
+ if (aip->ai_addr->sa_family != AF_INET &&
|
|
+ aip->ai_addr->sa_family != AF_INET6)
|
|
+ continue;
|
|
+
|
|
+ ctx->ns = xrealloc(ctx->ns, sizeof(*ctx->ns) * (ctx->n_ns + 1));
|
|
+ ctx->ns[ctx->n_ns].name = addr;
|
|
+ ctx->ns[ctx->n_ns].addr.len = aip->ai_addrlen;
|
|
+
|
|
+ memcpy(&ctx->ns[ctx->n_ns].addr.u.sa, aip->ai_addr, aip->ai_addrlen);
|
|
+
|
|
+ ctx->n_ns++;
|
|
+ }
|
|
+
|
|
+ freeaddrinfo(ai);
|
|
+
|
|
+ return &ctx->ns[ctx->n_ns];
|
|
+ }
|
|
+
|
|
+ return NULL;
|
|
+ }
|
|
+
|
|
+ ctx->ns = xrealloc(ctx->ns, sizeof(*ctx->ns) * (ctx->n_ns + 1));
|
|
+ ctx->ns[ctx->n_ns].addr = a;
|
|
+ ctx->ns[ctx->n_ns].name = addr;
|
|
+
|
|
+ return &ctx->ns[ctx->n_ns++];
|
|
+}
|
|
+
|
|
+static int
|
|
+parse_resolvconf(resolve_ctx_t *ctx)
|
|
+{
|
|
+ int prev_n_ns = ctx->n_ns;
|
|
+ char line[128], *p;
|
|
+ FILE *resolv;
|
|
+ bool ok;
|
|
+
|
|
+ if ((resolv = fopen("/etc/resolv.conf", "r")) != NULL) {
|
|
+ while (fgets(line, sizeof(line), resolv)) {
|
|
+ p = strtok(line, " \t\n");
|
|
+
|
|
+ if (!p || strcmp(p, "nameserver"))
|
|
+ continue;
|
|
+
|
|
+ p = strtok(NULL, " \t\n");
|
|
+
|
|
+ if (!p)
|
|
+ continue;
|
|
+
|
|
+ p = xstrdup(p);
|
|
+ ok = add_ns(ctx, p);
|
|
+
|
|
+ free(p);
|
|
+
|
|
+ if (!ok)
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ fclose(resolv);
|
|
+ }
|
|
+
|
|
+ return ctx->n_ns - prev_n_ns;
|
|
+}
|
|
+
|
|
+static query_t *
|
|
+add_query(resolve_ctx_t *ctx, int type, const char *dname)
|
|
+{
|
|
+ opt_rr_t *opt;
|
|
+ ssize_t qlen;
|
|
+
|
|
+ ctx->queries = xrealloc(ctx->queries, sizeof(*ctx->queries) * (ctx->n_queries + 1));
|
|
+
|
|
+ memset(&ctx->queries[ctx->n_queries], 0, sizeof(*ctx->queries));
|
|
+
|
|
+ qlen = res_mkquery(QUERY, dname, C_IN, type, NULL, 0, NULL,
|
|
+ ctx->queries[ctx->n_queries].query,
|
|
+ sizeof(ctx->queries[ctx->n_queries].query));
|
|
+
|
|
+ /* add OPT record */
|
|
+ if (ctx->edns_maxsize != 0 && qlen + sizeof(opt_rr_t) <= sizeof(ctx->queries[ctx->n_queries].query)) {
|
|
+ ctx->queries[ctx->n_queries].query[11] = 1;
|
|
+
|
|
+ opt = (opt_rr_t *)&ctx->queries[ctx->n_queries].query[qlen];
|
|
+ opt->root_domain = 0;
|
|
+ opt->type = htons(41);
|
|
+ opt->edns_maxsize = htons(ctx->edns_maxsize);
|
|
+ opt->extended_rcode = 0;
|
|
+ opt->edns_version = 0;
|
|
+ opt->z = htons(0);
|
|
+ opt->data_length = htons(0);
|
|
+
|
|
+ qlen += sizeof(opt_rr_t);
|
|
+ }
|
|
+
|
|
+ ctx->queries[ctx->n_queries].qlen = qlen;
|
|
+ ctx->queries[ctx->n_queries].name = xstrdup(dname);
|
|
+ ctx->queries[ctx->n_queries].rcode = -1;
|
|
+
|
|
+ return &ctx->queries[ctx->n_queries++];
|
|
+}
|
|
+
|
|
+static bool
|
|
+check_types(uc_value_t *typenames, uint32_t *types)
|
|
+{
|
|
+ size_t i;
|
|
+
|
|
+ *types = 0;
|
|
+
|
|
+ for_each_item(typenames, typename) {
|
|
+ if (ucv_type(typename) != UC_STRING)
|
|
+ err_return(EINVAL, "Query type value not a string");
|
|
+
|
|
+ for (i = 0; qtypes[i].name; i++) {
|
|
+ if (!strcasecmp(ucv_string_get(typename), qtypes[i].name)) {
|
|
+ *types |= (1 << i);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!qtypes[i].name)
|
|
+ err_return(EINVAL, "Unrecognized query type '%s'",
|
|
+ ucv_string_get(typename));
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+}
|
|
+
|
|
+static void
|
|
+add_queries(resolve_ctx_t *ctx, uc_value_t *name)
|
|
+{
|
|
+ char *s = ucv_string_get(name);
|
|
+ char *ptr;
|
|
+ size_t i;
|
|
+
|
|
+ if (ctx->qtypes == 0) {
|
|
+ ptr = make_ptr(s);
|
|
+
|
|
+ if (ptr) {
|
|
+ add_query(ctx, ns_t_ptr, ptr);
|
|
+ }
|
|
+ else {
|
|
+ add_query(ctx, ns_t_a, s);
|
|
+ add_query(ctx, ns_t_aaaa, s);
|
|
+ }
|
|
+ }
|
|
+ else {
|
|
+ for (i = 0; qtypes[i].name; i++) {
|
|
+ if (ctx->qtypes & (1 << i)) {
|
|
+ if (qtypes[i].type == ns_t_ptr) {
|
|
+ ptr = make_ptr(s);
|
|
+ add_query(ctx, ns_t_ptr, ptr ? ptr : s);
|
|
+ }
|
|
+ else {
|
|
+ add_query(ctx, qtypes[i].type, s);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+static bool
|
|
+parse_options(resolve_ctx_t *ctx, uc_value_t *opts)
|
|
+{
|
|
+ uc_value_t *v;
|
|
+
|
|
+ if (!check_types(ucv_object_get(opts, "type", NULL), &ctx->qtypes))
|
|
+ return false;
|
|
+
|
|
+ for_each_item(ucv_object_get(opts, "nameserver", NULL), server) {
|
|
+ if (ucv_type(server) != UC_STRING)
|
|
+ err_return(EINVAL, "Nameserver value not a string");
|
|
+
|
|
+ if (!add_ns(ctx, ucv_string_get(server)))
|
|
+ err_return(EINVAL, "Unable to resolve nameserver address '%s'",
|
|
+ ucv_string_get(server));
|
|
+ }
|
|
+
|
|
+ /* Find NS servers in resolv.conf if none provided */
|
|
+ if (ctx->n_ns == 0)
|
|
+ parse_resolvconf(ctx);
|
|
+
|
|
+ /* Fall back to localhost if we could not find NS in resolv.conf */
|
|
+ if (ctx->n_ns == 0)
|
|
+ add_ns(ctx, "127.0.0.1");
|
|
+
|
|
+ v = ucv_object_get(opts, "retries", NULL);
|
|
+
|
|
+ if (ucv_type(v) == UC_INTEGER)
|
|
+ ctx->retries = ucv_uint64_get(v);
|
|
+ else if (v)
|
|
+ err_return(EINVAL, "Retries value not an integer");
|
|
+
|
|
+ v = ucv_object_get(opts, "timeout", NULL);
|
|
+
|
|
+ if (ucv_type(v) == UC_INTEGER)
|
|
+ ctx->timeout = ucv_uint64_get(v);
|
|
+ else if (v)
|
|
+ err_return(EINVAL, "Timeout value not an integer");
|
|
+
|
|
+ v = ucv_object_get(opts, "edns_maxsize", NULL);
|
|
+
|
|
+ if (ucv_type(v) == UC_INTEGER)
|
|
+ ctx->edns_maxsize = ucv_uint64_get(v);
|
|
+ else if (v)
|
|
+ err_return(EINVAL, "EDNS max size not an integer");
|
|
+
|
|
+ return true;
|
|
+}
|
|
+
|
|
+static uc_value_t *
|
|
+uc_resolv_query(uc_vm_t *vm, size_t nargs)
|
|
+{
|
|
+ resolve_ctx_t ctx = { .retries = 2, .timeout = 5000, .edns_maxsize = 4096 };
|
|
+ uc_value_t *names = uc_fn_arg(0);
|
|
+ uc_value_t *opts = uc_fn_arg(1);
|
|
+ uc_value_t *res_obj = NULL;
|
|
+
|
|
+ if (!parse_options(&ctx, opts))
|
|
+ goto err;
|
|
+
|
|
+ for_each_item(names, name) {
|
|
+ if (ucv_type(name) != UC_STRING) {
|
|
+ set_error(EINVAL, "Domain name value not a string");
|
|
+ goto err;
|
|
+ }
|
|
+
|
|
+ add_queries(&ctx, name);
|
|
+ }
|
|
+
|
|
+ res_obj = ucv_object_new(vm);
|
|
+
|
|
+ if (send_queries(&ctx, vm, res_obj) == 0)
|
|
+ set_error(ETIMEDOUT, "Server did not respond");
|
|
+
|
|
+err:
|
|
+ while (ctx.n_queries)
|
|
+ free(ctx.queries[--ctx.n_queries].name);
|
|
+
|
|
+ free(ctx.queries);
|
|
+ free(ctx.ns);
|
|
+
|
|
+ return res_obj;
|
|
+}
|
|
+
|
|
+static uc_value_t *
|
|
+uc_resolv_error(uc_vm_t *vm, size_t nargs)
|
|
+{
|
|
+ uc_stringbuf_t *buf;
|
|
+ const char *s;
|
|
+
|
|
+ if (last_error.code == 0)
|
|
+ return NULL;
|
|
+
|
|
+ buf = ucv_stringbuf_new();
|
|
+
|
|
+ s = strerror(last_error.code);
|
|
+
|
|
+ ucv_stringbuf_addstr(buf, s, strlen(s));
|
|
+
|
|
+ if (last_error.msg)
|
|
+ ucv_stringbuf_printf(buf, ": %s", last_error.msg);
|
|
+
|
|
+ set_error(0, NULL);
|
|
+
|
|
+ return ucv_stringbuf_finish(buf);
|
|
+}
|
|
+
|
|
+
|
|
+static const uc_function_list_t resolv_fns[] = {
|
|
+ { "query", uc_resolv_query },
|
|
+ { "error", uc_resolv_error },
|
|
+};
|
|
+
|
|
+void uc_module_init(uc_vm_t *vm, uc_value_t *scope)
|
|
+{
|
|
+ uc_function_list_register(scope, resolv_fns);
|
|
+}
|