diff options
Diffstat (limited to 'src/libserver/dns.c')
-rw-r--r-- | src/libserver/dns.c | 1124 |
1 files changed, 1124 insertions, 0 deletions
diff --git a/src/libserver/dns.c b/src/libserver/dns.c new file mode 100644 index 0000000..be2d5a3 --- /dev/null +++ b/src/libserver/dns.c @@ -0,0 +1,1124 @@ +/* + * Copyright 2023 Vsevolod Stakhov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "contrib/librdns/rdns.h" +#include "config.h" +#include "dns.h" +#include "rspamd.h" +#include "utlist.h" +#include "contrib/libev/ev.h" +#include "contrib/librdns/rdns.h" +#include "contrib/librdns/dns_private.h" +#include "contrib/librdns/rdns_ev.h" +#include "unix-std.h" + +#include <unicode/uidna.h> + +static const gchar *M = "rspamd dns"; + +static struct rdns_upstream_elt *rspamd_dns_select_upstream(const char *name, + size_t len, void *ups_data); +static struct rdns_upstream_elt *rspamd_dns_select_upstream_retransmit( + const char *name, + size_t len, + struct rdns_upstream_elt *prev_elt, + void *ups_data); +static void rspamd_dns_upstream_ok(struct rdns_upstream_elt *elt, + void *ups_data); +static void rspamd_dns_upstream_fail(struct rdns_upstream_elt *elt, + void *ups_data, const gchar *reason); +static unsigned int rspamd_dns_upstream_count(void *ups_data); + +static struct rdns_upstream_context rspamd_ups_ctx = { + .select = rspamd_dns_select_upstream, + .select_retransmit = rspamd_dns_select_upstream_retransmit, + .ok = rspamd_dns_upstream_ok, + .fail = rspamd_dns_upstream_fail, + .count = rspamd_dns_upstream_count, + .data = NULL}; + +struct rspamd_dns_request_ud { + struct rspamd_async_session *session; + dns_callback_type cb; + gpointer ud; + rspamd_mempool_t *pool; + struct rspamd_task *task; + struct rspamd_symcache_dynamic_item *item; + struct rdns_request *req; + struct rdns_reply *reply; +}; + +struct rspamd_dns_fail_cache_entry { + const char *name; + gint32 namelen; + enum rdns_request_type type; +}; + +static const gint8 ascii_dns_table[128] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + /* HYPHEN-MINUS..FULL STOP */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, + /* 0..9 digits */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, + /* LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z */ + -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /* _ */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, 1, + /* LATIN SMALL LETTER A..LATIN SMALL LETTER Z */ + -1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1}; + +static guint +rspamd_dns_fail_hash(gconstpointer ptr) +{ + struct rspamd_dns_fail_cache_entry *elt = + (struct rspamd_dns_fail_cache_entry *) ptr; + + /* We don't care about type when doing hashing */ + return rspamd_cryptobox_fast_hash(elt->name, elt->namelen, + rspamd_hash_seed()); +} + +static gboolean +rspamd_dns_fail_equal(gconstpointer p1, gconstpointer p2) +{ + struct rspamd_dns_fail_cache_entry *e1 = (struct rspamd_dns_fail_cache_entry *) p1, + *e2 = (struct rspamd_dns_fail_cache_entry *) p2; + + if (e1->type == e2->type && e1->namelen == e2->namelen) { + return memcmp(e1->name, e2->name, e1->namelen) == 0; + } + + return FALSE; +} + +static void +rspamd_dns_fin_cb(gpointer arg) +{ + struct rspamd_dns_request_ud *reqdata = (struct rspamd_dns_request_ud *) arg; + + if (reqdata->item) { + rspamd_symcache_set_cur_item(reqdata->task, reqdata->item); + } + + if (reqdata->reply) { + reqdata->cb(reqdata->reply, reqdata->ud); + } + else { + struct rdns_reply fake_reply; + + memset(&fake_reply, 0, sizeof(fake_reply)); + fake_reply.code = RDNS_RC_TIMEOUT; + fake_reply.request = reqdata->req; + fake_reply.resolver = reqdata->req->resolver; + fake_reply.requested_name = reqdata->req->requested_names[0].name; + + reqdata->cb(&fake_reply, reqdata->ud); + } + + rdns_request_release(reqdata->req); + + if (reqdata->item) { + rspamd_symcache_item_async_dec_check(reqdata->task, + reqdata->item, M); + } + + if (reqdata->pool == NULL) { + g_free(reqdata); + } +} + +static void +rspamd_dns_callback(struct rdns_reply *reply, gpointer ud) +{ + struct rspamd_dns_request_ud *reqdata = ud; + + reqdata->reply = reply; + + + if (reqdata->session) { + if (reply->code == RDNS_RC_SERVFAIL && + reqdata->task && + reqdata->task->resolver->fails_cache) { + + /* Add to cache... */ + const gchar *name = reqdata->req->requested_names[0].name; + gchar *target; + gsize namelen; + struct rspamd_dns_fail_cache_entry *nentry; + + /* Allocate in a single entry to allow further free in a single call */ + namelen = strlen(name); + nentry = g_malloc(sizeof(nentry) + namelen + 1); + target = ((gchar *) nentry) + sizeof(nentry); + rspamd_strlcpy(target, name, namelen + 1); + nentry->type = reqdata->req->requested_names[0].type; + nentry->name = target; + nentry->namelen = namelen; + + /* Rdns request is retained there */ + rspamd_lru_hash_insert(reqdata->task->resolver->fails_cache, + nentry, rdns_request_retain(reply->request), + reqdata->task->task_timestamp, + reqdata->task->resolver->fails_cache_time); + } + + /* + * Ref event to avoid double unref by + * event removing + */ + rdns_request_retain(reply->request); + rspamd_session_remove_event(reqdata->session, + rspamd_dns_fin_cb, reqdata); + } + else { + reqdata->cb(reply, reqdata->ud); + + if (reqdata->pool == NULL) { + g_free(reqdata); + } + } +} + +struct rspamd_dns_request_ud * +rspamd_dns_resolver_request(struct rspamd_dns_resolver *resolver, + struct rspamd_async_session *session, + rspamd_mempool_t *pool, + dns_callback_type cb, + gpointer ud, + enum rdns_request_type type, + const char *name) +{ + struct rdns_request *req; + struct rspamd_dns_request_ud *reqdata = NULL; + guint nlen = strlen(name); + gchar *real_name = NULL; + + g_assert(resolver != NULL); + + if (resolver->r == NULL) { + return NULL; + } + + if (nlen == 0 || nlen > DNS_D_MAXNAME) { + return NULL; + } + + if (session && rspamd_session_blocked(session)) { + return NULL; + } + + if (rspamd_str_has_8bit(name, nlen)) { + /* Convert to idna using libicu as it follows all the standards */ + real_name = rspamd_dns_resolver_idna_convert_utf8(resolver, pool, + name, nlen, &nlen); + + if (real_name == NULL) { + return NULL; + } + + name = real_name; + } + + /* Name is now in ASCII only */ + for (gsize i = 0; i < nlen; i++) { + if (ascii_dns_table[((unsigned int) name[i]) & 0x7F] == -1) { + /* Invalid DNS name requested */ + + if (!pool) { + g_free(real_name); + } + + return NULL; + } + } + + if (pool != NULL) { + reqdata = + rspamd_mempool_alloc0(pool, sizeof(struct rspamd_dns_request_ud)); + } + else { + reqdata = g_malloc0(sizeof(struct rspamd_dns_request_ud)); + } + + reqdata->pool = pool; + reqdata->session = session; + reqdata->cb = cb; + reqdata->ud = ud; + + req = rdns_make_request_full(resolver->r, rspamd_dns_callback, reqdata, + resolver->request_timeout, resolver->max_retransmits, 1, name, + type); + reqdata->req = req; + + if (session) { + if (req != NULL) { + rspamd_session_add_event(session, + (event_finalizer_t) rspamd_dns_fin_cb, + reqdata, + M); + } + } + + if (req == NULL) { + if (pool == NULL) { + g_free(reqdata); + g_free(real_name); + } + + return NULL; + } + + if (real_name && pool == NULL) { + g_free(real_name); + } + + return reqdata; +} + +struct rspamd_dns_cached_delayed_cbdata { + struct rspamd_task *task; + dns_callback_type cb; + gpointer ud; + ev_timer tm; + struct rdns_request *req; +}; + +static void +rspamd_fail_cache_cb(EV_P_ ev_timer *w, int revents) +{ + struct rspamd_dns_cached_delayed_cbdata *cbd = + (struct rspamd_dns_cached_delayed_cbdata *) w->data; + struct rdns_reply fake_reply; + + ev_timer_stop(EV_A_ w); + memset(&fake_reply, 0, sizeof(fake_reply)); + fake_reply.code = RDNS_RC_SERVFAIL; + fake_reply.request = cbd->req; + fake_reply.resolver = cbd->req->resolver; + fake_reply.requested_name = cbd->req->requested_names[0].name; + cbd->cb(&fake_reply, cbd->ud); + rdns_request_release(cbd->req); +} + +static gboolean +make_dns_request_task_common(struct rspamd_task *task, + dns_callback_type cb, + gpointer ud, + enum rdns_request_type type, + const char *name, + gboolean forced) +{ + struct rspamd_dns_request_ud *reqdata; + + if (!forced && task->dns_requests >= task->cfg->dns_max_requests) { + return FALSE; + } + + if (task->resolver->fails_cache) { + /* Search in failures cache */ + struct rspamd_dns_fail_cache_entry search; + struct rdns_request *req; + + search.name = name; + search.namelen = strlen(name); + search.type = type; + + if ((req = rspamd_lru_hash_lookup(task->resolver->fails_cache, + &search, task->task_timestamp)) != NULL) { + /* + * We need to reply with SERVFAIL again to the API, so add a special + * timer, uh-oh, and fire it + */ + struct rspamd_dns_cached_delayed_cbdata *cbd = + rspamd_mempool_alloc0(task->task_pool, sizeof(*cbd)); + + ev_timer_init(&cbd->tm, rspamd_fail_cache_cb, 0.0, 0.0); + cbd->task = task; + cbd->cb = cb; + cbd->ud = ud; + cbd->req = rdns_request_retain(req); + cbd->tm.data = cbd; + + return TRUE; + } + } + + reqdata = rspamd_dns_resolver_request( + task->resolver, task->s, task->task_pool, cb, ud, + type, name); + + if (reqdata) { + task->dns_requests++; + + reqdata->task = task; + reqdata->item = rspamd_symcache_get_cur_item(task); + + if (reqdata->item) { + /* We are inside some session */ + rspamd_symcache_item_async_inc(task, reqdata->item, M); + } + + if (!forced && task->dns_requests >= task->cfg->dns_max_requests) { + msg_info_task("stop resolving on reaching %ud requests", + task->dns_requests); + } + + return TRUE; + } + + return FALSE; +} + +gboolean +rspamd_dns_resolver_request_task(struct rspamd_task *task, + dns_callback_type cb, + gpointer ud, + enum rdns_request_type type, + const char *name) +{ + return make_dns_request_task_common(task, cb, ud, type, name, FALSE); +} + +gboolean +rspamd_dns_resolver_request_task_forced(struct rspamd_task *task, + dns_callback_type cb, + gpointer ud, + enum rdns_request_type type, + const char *name) +{ + return make_dns_request_task_common(task, cb, ud, type, name, TRUE); +} + +static void rspamd_rnds_log_bridge( + void *log_data, + enum rdns_log_level level, + const char *function, + const char *format, + va_list args) +{ + rspamd_logger_t *logger = log_data; + + rspamd_common_logv(logger, (GLogLevelFlags) level, "rdns", NULL, + function, format, args); +} + +static void +rspamd_dns_server_init(struct upstream *up, guint idx, gpointer ud) +{ + struct rspamd_dns_resolver *r = ud; + rspamd_inet_addr_t *addr; + void *serv; + struct rdns_upstream_elt *elt; + + addr = rspamd_upstream_addr_next(up); + + if (r->cfg) { + serv = rdns_resolver_add_server(r->r, rspamd_inet_address_to_string(addr), + rspamd_inet_address_get_port(addr), 0, r->cfg->dns_io_per_server); + + elt = rspamd_mempool_alloc0(r->cfg->cfg_pool, sizeof(*elt)); + elt->server = serv; + elt->lib_data = up; + + rspamd_upstream_set_data(up, elt); + } + else { + serv = rdns_resolver_add_server(r->r, rspamd_inet_address_to_string(addr), + rspamd_inet_address_get_port(addr), 0, 8); + } + + g_assert(serv != NULL); +} + +static void +rspamd_dns_server_reorder(struct upstream *up, guint idx, gpointer ud) +{ + struct rspamd_dns_resolver *r = ud; + + rspamd_upstream_set_weight(up, rspamd_upstreams_count(r->ups) - idx + 1); +} + +static bool +rspamd_dns_resolv_conf_on_server(struct rdns_resolver *resolver, + const char *name, unsigned int port, + int priority, unsigned int io_cnt, void *ud) +{ + struct rspamd_dns_resolver *dns_resolver = ud; + struct rspamd_config *cfg; + rspamd_inet_addr_t *addr; + gint test_fd; + + cfg = dns_resolver->cfg; + + msg_info_config("parsed nameserver %s from resolv.conf", name); + + /* Try to open a connection */ + if (!rspamd_parse_inet_address(&addr, name, strlen(name), + RSPAMD_INET_ADDRESS_PARSE_DEFAULT)) { + msg_warn_config("cannot parse nameserver address %s", name); + + return FALSE; + } + + rspamd_inet_address_set_port(addr, port); + test_fd = rspamd_inet_address_connect(addr, SOCK_DGRAM, TRUE); + + if (test_fd == -1 && (errno != EINTR || errno != ECONNREFUSED || errno != ECONNRESET)) { + msg_info_config("cannot open connection to nameserver at address %s: %s", + name, strerror(errno)); + rspamd_inet_address_free(addr); + + return FALSE; + } + + rspamd_inet_address_free(addr); + close(test_fd); + + return rspamd_upstreams_add_upstream(dns_resolver->ups, name, port, + RSPAMD_UPSTREAM_PARSE_NAMESERVER, + NULL); +} + +static void +rspamd_process_fake_reply(struct rspamd_config *cfg, + struct rspamd_dns_resolver *dns_resolver, + const ucl_object_t *cur_arr) +{ + const ucl_object_t *cur; + ucl_object_iter_t it; + + it = ucl_object_iterate_new(cur_arr); + + while ((cur = ucl_object_iterate_safe(it, true))) { + const ucl_object_t *type_obj, *name_obj, *code_obj, *replies_obj; + enum rdns_request_type rtype = RDNS_REQUEST_A; + enum dns_rcode rcode = RDNS_RC_NOERROR; + struct rdns_reply_entry *replies = NULL; + const gchar *name = NULL; + + if (ucl_object_type(cur) != UCL_OBJECT) { + continue; + } + + name_obj = ucl_object_lookup(cur, "name"); + if (name_obj == NULL || + (name = ucl_object_tostring(name_obj)) == NULL) { + msg_err_config("no name for fake dns reply"); + continue; + } + + type_obj = ucl_object_lookup(cur, "type"); + if (type_obj) { + rtype = rdns_type_fromstr(ucl_object_tostring(type_obj)); + + if (rtype == RDNS_REQUEST_INVALID) { + msg_err_config("invalid type for %s: %s", name, + ucl_object_tostring(type_obj)); + continue; + } + } + + code_obj = ucl_object_lookup_any(cur, "code", "rcode", NULL); + if (code_obj) { + rcode = rdns_rcode_fromstr(ucl_object_tostring(code_obj)); + + if (rcode == RDNS_RC_INVALID) { + msg_err_config("invalid rcode for %s: %s", name, + ucl_object_tostring(code_obj)); + continue; + } + } + + if (rcode == RDNS_RC_NOERROR) { + /* We want replies to be set for this rcode */ + replies_obj = ucl_object_lookup(cur, "replies"); + + if (replies_obj == NULL || ucl_object_type(replies_obj) != UCL_ARRAY) { + msg_err_config("invalid replies for fake DNS record %s", name); + continue; + } + + ucl_object_iter_t rep_it; + const ucl_object_t *rep_obj; + + rep_it = ucl_object_iterate_new(replies_obj); + + while ((rep_obj = ucl_object_iterate_safe(rep_it, true))) { + const gchar *str_rep = ucl_object_tostring(rep_obj); + struct rdns_reply_entry *rep; + gchar **svec; + + if (str_rep == NULL) { + msg_err_config("invalid reply element for fake DNS record %s", + name); + continue; + } + + rep = calloc(1, sizeof(*rep)); + g_assert(rep != NULL); + + rep->type = rtype; + rep->ttl = 0; + + switch (rtype) { + case RDNS_REQUEST_A: + if (inet_pton(AF_INET, str_rep, &rep->content.a.addr) != 1) { + msg_err_config("invalid A reply element for fake " + "DNS record %s: %s", + name, str_rep); + free(rep); + } + else { + DL_APPEND(replies, rep); + } + break; + case RDNS_REQUEST_NS: + rep->content.ns.name = strdup(str_rep); + DL_APPEND(replies, rep); + break; + case RDNS_REQUEST_PTR: + rep->content.ptr.name = strdup(str_rep); + DL_APPEND(replies, rep); + break; + case RDNS_REQUEST_MX: + svec = g_strsplit_set(str_rep, " :", -1); + + if (svec && svec[0] && svec[1]) { + rep->content.mx.priority = strtoul(svec[0], NULL, 10); + rep->content.mx.name = strdup(svec[1]); + DL_APPEND(replies, rep); + } + else { + msg_err_config("invalid MX reply element for fake " + "DNS record %s: %s", + name, str_rep); + free(rep); + } + + g_strfreev(svec); + break; + case RDNS_REQUEST_TXT: + rep->content.txt.data = strdup(str_rep); + DL_APPEND(replies, rep); + break; + case RDNS_REQUEST_SOA: + svec = g_strsplit_set(str_rep, " :", -1); + + /* 7 elements */ + if (svec && svec[0] && svec[1] && svec[2] && + svec[3] && svec[4] && svec[5] && svec[6]) { + rep->content.soa.mname = strdup(svec[0]); + rep->content.soa.admin = strdup(svec[1]); + rep->content.soa.serial = strtoul(svec[2], NULL, 10); + rep->content.soa.refresh = strtol(svec[3], NULL, 10); + rep->content.soa.retry = strtol(svec[4], NULL, 10); + rep->content.soa.expire = strtol(svec[5], NULL, 10); + rep->content.soa.minimum = strtoul(svec[6], NULL, 10); + DL_APPEND(replies, rep); + } + else { + msg_err_config("invalid MX reply element for fake " + "DNS record %s: %s", + name, str_rep); + free(rep); + } + + g_strfreev(svec); + break; + case RDNS_REQUEST_AAAA: + if (inet_pton(AF_INET6, str_rep, &rep->content.aaa.addr) != 1) { + msg_err_config("invalid AAAA reply element for fake " + "DNS record %s: %s", + name, str_rep); + free(rep); + } + else { + DL_APPEND(replies, rep); + } + break; + case RDNS_REQUEST_SRV: + default: + msg_err_config("invalid or unsupported reply element " + "for fake DNS record %s(%s): %s", + name, rdns_str_from_type(rtype), str_rep); + free(rep); + break; + } + } + + ucl_object_iterate_free(rep_it); + + if (replies) { + struct rdns_reply_entry *tmp_entry; + guint i = 0; + DL_COUNT(replies, tmp_entry, i); + + msg_info_config("added fake record: %s(%s); %d replies", name, + rdns_str_from_type(rtype), i); + rdns_resolver_set_fake_reply(dns_resolver->r, + name, rtype, rcode, replies); + } + else { + msg_warn_config("record %s has no replies, not adding", + name); + } + } + else { + /* This entry returns some non valid code, no replies are possible */ + replies_obj = ucl_object_lookup(cur, "replies"); + + if (replies_obj) { + msg_warn_config("replies are set for non-successful return " + "code for %s(%s), they will be ignored", + name, rdns_str_from_type(rtype)); + } + + rdns_resolver_set_fake_reply(dns_resolver->r, + name, rtype, rcode, NULL); + } + } + + ucl_object_iterate_free(it); +} + +static bool +rspamd_dns_read_hosts_file(struct rspamd_config *cfg, + struct rspamd_dns_resolver *dns_resolver, + const gchar *fname) +{ + gchar *linebuf = NULL; + gsize buflen = 0; + gssize r; + FILE *fp; + guint nadded = 0; + + fp = fopen(fname, "r"); + + if (fp == NULL) { + /* Hack to reduce noise */ + if (strcmp(fname, "/etc/hosts") == 0) { + msg_info_config("cannot open hosts file %s: %s", fname, + strerror(errno)); + } + else { + msg_err_config("cannot open hosts file %s: %s", fname, + strerror(errno)); + } + + return false; + } + + while ((r = getline(&linebuf, &buflen, fp)) > 0) { + if (linebuf[0] == '#' || g_ascii_isspace(linebuf[0])) { + /* Skip comment or empty line */ + continue; + } + + g_strchomp(linebuf); + + gchar **elts = g_strsplit_set(linebuf, " \t\v", -1); + rspamd_inet_addr_t *addr; + + if (!rspamd_parse_inet_address(&addr, elts[0], strlen(elts[0]), + RSPAMD_INET_ADDRESS_PARSE_REMOTE | RSPAMD_INET_ADDRESS_PARSE_NO_UNIX)) { + msg_warn_config("bad hosts file line: %s; cannot parse address", linebuf); + } + else { + /* Add all FQDN + aliases if any */ + gchar **cur_name = &elts[1]; + + while (*cur_name) { + if (strlen(*cur_name) == 0) { + cur_name++; + continue; + } + + if (*cur_name[0] == '#') { + /* Start of the comment */ + break; + } + + struct rdns_reply_entry *rep; + rep = calloc(1, sizeof(*rep)); + g_assert(rep != NULL); + + rep->ttl = 0; + + if (rspamd_inet_address_get_af(addr) == AF_INET) { + socklen_t unused; + const struct sockaddr_in *sin = (const struct sockaddr_in *) + rspamd_inet_address_get_sa(addr, &unused); + rep->type = RDNS_REQUEST_A; + memcpy(&rep->content.a.addr, &sin->sin_addr, + sizeof(rep->content.a.addr)); + } + else { + socklen_t unused; + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *) + rspamd_inet_address_get_sa(addr, &unused); + rep->type = RDNS_REQUEST_AAAA; + memcpy(&rep->content.aaa.addr, &sin6->sin6_addr, + sizeof(rep->content.aaa.addr)); + } + + rep->next = NULL; + rep->prev = rep; + rdns_resolver_set_fake_reply(dns_resolver->r, + *cur_name, rep->type, RDNS_RC_NOERROR, rep); + msg_debug_config("added fake record %s -> %s from hosts file %s", + *cur_name, rspamd_inet_address_to_string(addr), fname); + cur_name++; + nadded++; + } + + rspamd_inet_address_free(addr); + } + + g_strfreev(elts); + } + + if (linebuf) { + free(linebuf); + } + + msg_info_config("processed host file %s; %d records added", fname, nadded); + fclose(fp); + + return true; +} + +static void +rspamd_dns_resolver_config_ucl(struct rspamd_config *cfg, + struct rspamd_dns_resolver *dns_resolver, + const ucl_object_t *dns_section) +{ + const ucl_object_t *fake_replies, *fails_cache_size, *fails_cache_time, + *hosts; + static const ev_tstamp default_fails_cache_time = 10.0; + + /* Process fake replies */ + fake_replies = ucl_object_lookup_any(dns_section, "fake_records", + "fake_replies", NULL); + + if (fake_replies && ucl_object_type(fake_replies) == UCL_ARRAY) { + const ucl_object_t *cur_arr; + + DL_FOREACH(fake_replies, cur_arr) + { + rspamd_process_fake_reply(cfg, dns_resolver, cur_arr); + } + } + + hosts = ucl_object_lookup(dns_section, "hosts"); + + if (hosts == NULL) { + /* Read normal `/etc/hosts` file */ + rspamd_dns_read_hosts_file(cfg, dns_resolver, "/etc/hosts"); + } + else if (ucl_object_type(hosts) == UCL_NULL) { + /* Do nothing, hosts are explicitly disabled */ + } + else if (ucl_object_type(hosts) == UCL_STRING) { + if (!rspamd_dns_read_hosts_file(cfg, dns_resolver, ucl_object_tostring(hosts))) { + msg_err_config("cannot read hosts file %s", ucl_object_tostring(hosts)); + } + } + else if (ucl_object_type(hosts) == UCL_ARRAY) { + const ucl_object_t *cur; + ucl_object_iter_t it = NULL; + + while ((cur = ucl_object_iterate(hosts, &it, true)) != NULL) { + if (!rspamd_dns_read_hosts_file(cfg, dns_resolver, ucl_object_tostring(cur))) { + msg_err_config("cannot read hosts file %s", ucl_object_tostring(cur)); + } + } + } + else { + msg_err_config("invalid type for hosts parameter: %s", + ucl_object_type_to_string(ucl_object_type(hosts))); + } + + fails_cache_size = ucl_object_lookup(dns_section, "fails_cache_size"); + if (fails_cache_size && ucl_object_type(fails_cache_size) == UCL_INT) { + + dns_resolver->fails_cache_time = default_fails_cache_time; + fails_cache_time = ucl_object_lookup(dns_section, "fails_cache_time"); + + if (fails_cache_time) { + dns_resolver->fails_cache_time = ucl_object_todouble(fails_cache_time); + } + + dns_resolver->fails_cache = rspamd_lru_hash_new_full( + ucl_object_toint(fails_cache_size), + g_free, (GDestroyNotify) rdns_request_release, + rspamd_dns_fail_hash, rspamd_dns_fail_equal); + } +} + +struct rspamd_dns_resolver * +rspamd_dns_resolver_init(rspamd_logger_t *logger, + struct ev_loop *ev_base, + struct rspamd_config *cfg) +{ + struct rspamd_dns_resolver *dns_resolver; + + dns_resolver = g_malloc0(sizeof(struct rspamd_dns_resolver)); + dns_resolver->event_loop = ev_base; + + if (cfg != NULL) { + dns_resolver->request_timeout = cfg->dns_timeout; + dns_resolver->max_retransmits = cfg->dns_retransmits; + } + else { + dns_resolver->request_timeout = 1; + dns_resolver->max_retransmits = 2; + } + + /* IDN translation is performed in Rspamd now */ + dns_resolver->r = rdns_resolver_new(RDNS_RESOLVER_NOIDN); + + UErrorCode uc_err = U_ZERO_ERROR; + + dns_resolver->uidna = uidna_openUTS46(UIDNA_DEFAULT, &uc_err); + g_assert(!U_FAILURE(uc_err)); + rdns_bind_libev(dns_resolver->r, dns_resolver->event_loop); + + if (cfg != NULL) { + rdns_resolver_set_log_level(dns_resolver->r, cfg->log_level); + dns_resolver->cfg = cfg; + rdns_resolver_set_dnssec(dns_resolver->r, cfg->enable_dnssec); + + if (cfg->nameservers == NULL) { + /* Parse resolv.conf */ + dns_resolver->ups = rspamd_upstreams_create(cfg->ups_ctx); + rspamd_upstreams_set_flags(dns_resolver->ups, + RSPAMD_UPSTREAM_FLAG_NORESOLVE); + rspamd_upstreams_set_rotation(dns_resolver->ups, + RSPAMD_UPSTREAM_MASTER_SLAVE); + + if (!rdns_resolver_parse_resolv_conf_cb(dns_resolver->r, + "/etc/resolv.conf", + rspamd_dns_resolv_conf_on_server, + dns_resolver)) { + msg_err("cannot parse resolv.conf and no nameservers defined, " + "so no ways to resolve addresses"); + rdns_resolver_release(dns_resolver->r); + dns_resolver->r = NULL; + + return dns_resolver; + } + + /* Use normal resolv.conf rules */ + rspamd_upstreams_foreach(dns_resolver->ups, rspamd_dns_server_reorder, + dns_resolver); + } + else { + dns_resolver->ups = rspamd_upstreams_create(cfg->ups_ctx); + rspamd_upstreams_set_flags(dns_resolver->ups, + RSPAMD_UPSTREAM_FLAG_NORESOLVE); + + if (!rspamd_upstreams_from_ucl(dns_resolver->ups, cfg->nameservers, + 53, dns_resolver)) { + msg_err_config("cannot parse DNS nameservers definitions"); + rdns_resolver_release(dns_resolver->r); + dns_resolver->r = NULL; + + return dns_resolver; + } + } + + rspamd_upstreams_foreach(dns_resolver->ups, rspamd_dns_server_init, + dns_resolver); + rdns_resolver_set_upstream_lib(dns_resolver->r, &rspamd_ups_ctx, + dns_resolver->ups); + cfg->dns_resolver = dns_resolver; + + if (cfg->cfg_ucl_obj) { + /* Configure additional options */ + const ucl_object_t *opts_section, *dns_section, *tmp; + + opts_section = ucl_object_lookup(cfg->cfg_ucl_obj, "options"); + + if (opts_section) { + /* TODO: implement a more simple merge logic */ + DL_FOREACH(opts_section, tmp) + { + dns_section = ucl_object_lookup(opts_section, "dns"); + + if (dns_section) { + rspamd_dns_resolver_config_ucl(cfg, dns_resolver, + dns_section); + } + } + } + } + } + + rdns_resolver_set_logger(dns_resolver->r, rspamd_rnds_log_bridge, logger); + rdns_resolver_init(dns_resolver->r); + + return dns_resolver; +} + +void rspamd_dns_resolver_deinit(struct rspamd_dns_resolver *resolver) +{ + if (resolver) { + if (resolver->r) { + rdns_resolver_release(resolver->r); + } + + if (resolver->ups) { + rspamd_upstreams_destroy(resolver->ups); + } + + if (resolver->fails_cache) { + rspamd_lru_hash_destroy(resolver->fails_cache); + } + + uidna_close(resolver->uidna); + + g_free(resolver); + } +} + + +static struct rdns_upstream_elt * +rspamd_dns_select_upstream(const char *name, + size_t len, void *ups_data) +{ + struct upstream_list *ups = ups_data; + struct upstream *up; + + up = rspamd_upstream_get(ups, RSPAMD_UPSTREAM_ROUND_ROBIN, name, len); + + if (up) { + msg_debug("select %s", rspamd_upstream_name(up)); + + return rspamd_upstream_get_data(up); + } + + return NULL; +} + +static struct rdns_upstream_elt * +rspamd_dns_select_upstream_retransmit( + const char *name, + size_t len, + struct rdns_upstream_elt *prev_elt, + void *ups_data) +{ + struct upstream_list *ups = ups_data; + struct upstream *up; + + if (prev_elt) { + up = rspamd_upstream_get_except(ups, (struct upstream *) prev_elt->lib_data, + RSPAMD_UPSTREAM_MASTER_SLAVE, name, len); + } + else { + up = rspamd_upstream_get_forced(ups, RSPAMD_UPSTREAM_RANDOM, name, len); + } + + if (up) { + msg_debug("select forced %s", rspamd_upstream_name(up)); + + return rspamd_upstream_get_data(up); + } + + return NULL; +} + +static void +rspamd_dns_upstream_ok(struct rdns_upstream_elt *elt, + void *ups_data) +{ + struct upstream *up = elt->lib_data; + + rspamd_upstream_ok(up); +} + +static void +rspamd_dns_upstream_fail(struct rdns_upstream_elt *elt, + void *ups_data, const gchar *reason) +{ + struct upstream *up = elt->lib_data; + + rspamd_upstream_fail(up, FALSE, reason); +} + +static unsigned int +rspamd_dns_upstream_count(void *ups_data) +{ + struct upstream_list *ups = ups_data; + + return rspamd_upstreams_alive(ups); +} + +gchar * +rspamd_dns_resolver_idna_convert_utf8(struct rspamd_dns_resolver *resolver, + rspamd_mempool_t *pool, + const char *name, + gint namelen, + guint *outlen) +{ + if (resolver == NULL || resolver->uidna == NULL || name == NULL || namelen > DNS_D_MAXNAME) { + return NULL; + } + + guint dest_len; + UErrorCode uc_err = U_ZERO_ERROR; + UIDNAInfo info = UIDNA_INFO_INITIALIZER; + /* Calculate length required */ + dest_len = uidna_nameToASCII_UTF8(resolver->uidna, name, namelen, + NULL, 0, &info, &uc_err); + + if (uc_err == U_BUFFER_OVERFLOW_ERROR) { + gchar *dest; + + if (pool) { + dest = rspamd_mempool_alloc(pool, dest_len + 1); + } + else { + dest = g_malloc(dest_len + 1); + } + + uc_err = U_ZERO_ERROR; + + dest_len = uidna_nameToASCII_UTF8(resolver->uidna, name, namelen, + dest, dest_len + 1, &info, &uc_err); + + if (U_FAILURE(uc_err)) { + + if (!pool) { + g_free(dest); + } + + return NULL; + } + + dest[dest_len] = '\0'; + + if (outlen) { + *outlen = dest_len; + } + + return dest; + } + + return NULL; +}
\ No newline at end of file |