/* * Copyright (c) 2008-2024 OARC, Inc. * Copyright (c) 2007-2008, Internet Systems Consortium, Inc. * Copyright (c) 2003-2007, The Measurement Factory, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. Neither the name of the copyright holder nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "response_time_index.h" #include "hashtbl.h" #include "inX_addr.h" #include "xmalloc.h" #include "syslog_debug.h" #include "pcap.h" #include "compat.h" #include #include #define TIMED_OUT 0 #define MISSING_QUERY 1 #define DROPPED_QUERY 2 #define INTERNAL_ERROR 3 #define FIRST_BUCKET 4 struct query; struct query { struct query * prev, *next; transport_message tm; dns_message m; }; #define MAX_ARRAY_SZ 65536 static hashtbl* theHash = 0; static enum response_time_mode mode = response_time_log10; static time_t max_sec = 5; static enum response_time_max_sec_mode max_sec_mode = response_time_ceil; static unsigned int bucket_size = 100; static size_t max_queries = 1000000, num_queries = 0; static struct query * qfirst = 0, *qlast = 0; static int max_iter = INTERNAL_ERROR, next_iter, flushing = 0; static enum response_time_full_mode full_mode = response_time_drop_query; void response_time_set_mode(enum response_time_mode m) { mode = m; } void response_time_set_max_sec(time_t s) { max_sec = s; } void response_time_set_max_sec_mode(enum response_time_max_sec_mode m) { max_sec_mode = m; } void response_time_set_bucket_size(unsigned int s) { bucket_size = s; } void response_time_set_max_queries(size_t q) { max_queries = q; } void response_time_set_full_mode(enum response_time_full_mode m) { full_mode = m; } static unsigned int _hash(const struct query* q) { if (q->m.qr) return inXaddr_hash(&q->tm.dst_ip_addr) ^ ((q->tm.dst_port & 0xffff) | (q->m.id << 16)); return inXaddr_hash(&q->tm.src_ip_addr) ^ ((q->tm.src_port & 0xffff) | (q->m.id << 16)); } static int _cmp(const struct query* a, const struct query* b) { int cmp; // compare DNS ID if (a->m.id != b->m.id) return a->m.id < b->m.id ? -1 : 1; // compare IP version, since inXaddr_cmp() does not, and protocol if (a->tm.ip_version != b->tm.ip_version) return a->tm.ip_version < b->tm.ip_version ? -1 : 1; if (a->tm.proto != b->tm.proto) return a->tm.proto < b->tm.proto ? -1 : 1; // compare client IP&port first since other should be server and more static data if (!a->m.qr && !b->m.qr) { // both are queries, compare source address first as that is the client if ((cmp = inXaddr_cmp(&a->tm.src_ip_addr, &b->tm.src_ip_addr))) return cmp; if (a->tm.src_port != b->tm.src_port) return a->tm.src_port < b->tm.src_port ? -1 : 1; if ((cmp = inXaddr_cmp(&a->tm.dst_ip_addr, &b->tm.dst_ip_addr))) return cmp; if (a->tm.dst_port != b->tm.dst_port) return a->tm.dst_port < b->tm.dst_port ? -1 : 1; } else if (a->m.qr && b->m.qr) { // both are responses, compare destination address first as that is the client if ((cmp = inXaddr_cmp(&a->tm.dst_ip_addr, &b->tm.dst_ip_addr))) return cmp; if (a->tm.dst_port != b->tm.dst_port) return a->tm.dst_port < b->tm.dst_port ? -1 : 1; if ((cmp = inXaddr_cmp(&a->tm.src_ip_addr, &b->tm.src_ip_addr))) return cmp; if (a->tm.src_port != b->tm.src_port) return a->tm.src_port < b->tm.src_port ? -1 : 1; } else if (a->m.qr && !b->m.qr) { // a is a response and b is a query, compare a's destination with b's source first if ((cmp = inXaddr_cmp(&a->tm.dst_ip_addr, &b->tm.src_ip_addr))) return cmp; if (a->tm.dst_port != b->tm.src_port) return a->tm.dst_port < b->tm.src_port ? -1 : 1; if ((cmp = inXaddr_cmp(&a->tm.src_ip_addr, &b->tm.dst_ip_addr))) return cmp; if (a->tm.src_port != b->tm.dst_port) return a->tm.src_port < b->tm.dst_port ? -1 : 1; } else { // a is a query and b is a response, compare a's source with b's destination first if ((cmp = inXaddr_cmp(&a->tm.src_ip_addr, &b->tm.dst_ip_addr))) return cmp; if (a->tm.src_port != b->tm.dst_port) return a->tm.src_port < b->tm.dst_port ? -1 : 1; if ((cmp = inXaddr_cmp(&a->tm.dst_ip_addr, &b->tm.src_ip_addr))) return cmp; if (a->tm.dst_port != b->tm.src_port) return a->tm.dst_port < b->tm.src_port ? -1 : 1; } return 0; } int response_time_indexer(const dns_message* m) { struct query q, *obj; transport_message* tm = m->tm; int ret = -1; if (flushing) { dfprintf(1, "response_time: flushing %u %s", m->id, m->qname); return TIMED_OUT; } if (m->malformed) { return -1; } dfprintf(1, "response_time: %s %u %s", m->qr ? "response" : "query", m->id, m->qname); if (!theHash) { theHash = hash_create(MAX_ARRAY_SZ, (hashfunc*)_hash, (hashkeycmp*)_cmp, 0, 0, 0); if (!theHash) return INTERNAL_ERROR; } q.m = *m; q.tm = *tm; q.m.tm = &q.tm; q.m.tld = 0; obj = hash_find(&q, theHash); if (m->qr) { struct timeval diff; unsigned long us; int iter; if (!obj) { // got a response without a query, dfprint(1, "response_time: missing query for response"); return MISSING_QUERY; } // TODO: compare more? // - qclass/qtype, qname // found query, remove and calculate index if (obj->prev) obj->prev->next = obj->next; if (obj->next) obj->next->prev = obj->prev; if (obj == qfirst) qfirst = obj->next; if (obj == qlast) qlast = obj->prev; hash_remove(obj, theHash); num_queries--; q = *obj; q.m.tm = &q.tm; xfree(obj); diff.tv_sec = tm->ts.tv_sec - q.tm.ts.tv_sec; diff.tv_usec = tm->ts.tv_usec - q.tm.ts.tv_usec; if (diff.tv_usec >= 1000000) { diff.tv_sec += 1; diff.tv_usec -= 1000000; } else if (diff.tv_usec < 0) { diff.tv_sec -= 1; diff.tv_usec += 1000000; } if (diff.tv_sec < 0 || diff.tv_usec < 0) { dfprintf(1, "response_time: bad diff " PRItime ", " PRItime " - " PRItime, diff.tv_sec, diff.tv_usec, q.tm.ts.tv_sec, q.tm.ts.tv_usec, tm->ts.tv_sec, tm->ts.tv_usec); return INTERNAL_ERROR; } if (diff.tv_sec >= max_sec) { switch (max_sec_mode) { case response_time_ceil: dfprintf(2, "response_time: diff " PRItime " ceiled to " PRItime, diff.tv_sec, diff.tv_usec, max_sec, 0L); diff.tv_sec = max_sec; diff.tv_usec = 0; break; case response_time_timed_out: dfprintf(1, "response_time: diff " PRItime " too old, timed out", diff.tv_sec, diff.tv_usec); return TIMED_OUT; default: dfprint(1, "response_time: bad max_sec_mode"); return INTERNAL_ERROR; } } us = (diff.tv_sec * 1000000) + diff.tv_usec; switch (mode) { case response_time_bucket: iter = FIRST_BUCKET + (us / bucket_size); dfprintf(2, "response_time: found q/r us:%lu, put in bucket %d (%lu-%lu usec)", us, iter, (us / bucket_size) * bucket_size, ((us / bucket_size) + 1) * bucket_size); break; case response_time_log10: { double d = log10((double)us); if (d < 0) { dfprintf(1, "response_time: bad log10(%lu) ret %f", us, d); return INTERNAL_ERROR; } iter = FIRST_BUCKET + (int)d; dfprintf(2, "response_time: found q/r us:%lu, log10 %d (%.0f-%.0f usec)", us, iter, pow(10, (int)d), pow(10, (int)d + 1)); break; } case response_time_log2: { double d = log2((double)us); if (d < 0) { dfprintf(1, "response_time: bad log2(%lu) ret %f", us, d); return INTERNAL_ERROR; } iter = FIRST_BUCKET + (int)d; dfprintf(2, "response_time: found q/r us:%lu, log2 %d (%.0f-%.0f usec)", us, iter, pow(2, (int)d), pow(2, (int)d + 1)); break; } default: dfprint(1, "response_time: bad mode"); return INTERNAL_ERROR; } if (iter > max_iter) max_iter = iter; return iter; } if (obj) { // Found another query in the hash so the old one have timed out, // reuse the obj for the new query obj->tm.ts = tm->ts; if (obj != qlast) { if (obj->prev) obj->prev->next = obj->next; if (obj->next) { if (obj == qfirst) qfirst = obj->next; obj->next->prev = obj->prev; } obj->prev = qlast; obj->next = 0; assert(qlast); qlast->next = obj; qlast = obj; } dfprintf(1, "response_time: reuse %p, timed out", obj); return TIMED_OUT; } if (num_queries >= max_queries) { // We're at max, see if we can time out the oldest query ret = TIMED_OUT; assert(qfirst); if (tm->ts.tv_sec - qfirst->tm.ts.tv_sec < max_sec) { // no, so what to do? switch (full_mode) { case response_time_drop_query: dfprint(1, "response_time: full and oldest not old enough"); return DROPPED_QUERY; case response_time_drop_oldest: ret = DROPPED_QUERY; dfprint(2, "response_time: full and dropping oldest"); break; default: dfprint(1, "response_time: bad full_mode"); return INTERNAL_ERROR; } } // remove oldest obj from hash and reuse it obj = qfirst; qfirst = obj->next; if (qfirst) qfirst->prev = 0; hash_remove(obj, theHash); num_queries--; dfprintf(1, "response_time: reuse %p, too old", obj); } else { obj = xcalloc(1, sizeof(*obj)); if (!obj) { dfprint(1, "response_time: failed to alloc obj"); return INTERNAL_ERROR; } } *obj = q; obj->m.tm = &obj->tm; if (hash_add(obj, obj, theHash)) { xfree(obj); dfprint(1, "response_time: failed to add to hash"); return INTERNAL_ERROR; } obj->prev = qlast; obj->next = 0; if (qlast) qlast->next = obj; qlast = obj; if (!qfirst) qfirst = obj; num_queries++; dfprintf(2, "response_time: add %p, %zu/%zu queries", obj, num_queries, max_queries); return ret; } int response_time_iterator(const char** label) { static char label_buf[128]; if (!label) { next_iter = 0; return max_iter + 1; } if (next_iter > max_iter) { return -1; } if (next_iter < FIRST_BUCKET) { switch (next_iter) { case TIMED_OUT: *label = "timeouts"; break; case MISSING_QUERY: *label = "missing_queries"; break; case DROPPED_QUERY: *label = "dropped_queries"; break; case INTERNAL_ERROR: *label = "internal_errors"; break; default: return -1; } } else { switch (mode) { case response_time_bucket: snprintf(label_buf, 128, "%d-%d", (next_iter - FIRST_BUCKET) * bucket_size, (next_iter - FIRST_BUCKET + 1) * bucket_size); break; case response_time_log10: snprintf(label_buf, 128, "%.0f-%.0f", pow(10, next_iter - FIRST_BUCKET), pow(10, next_iter - FIRST_BUCKET + 1)); break; case response_time_log2: snprintf(label_buf, 128, "%.0f-%.0f", pow(2, next_iter - FIRST_BUCKET), pow(2, next_iter - FIRST_BUCKET + 1)); break; default: return -1; } *label = label_buf; } return next_iter++; } void response_time_reset() { max_iter = INTERNAL_ERROR; } static struct query* flushed_obj = 0; const dns_message* response_time_flush(enum flush_mode fm) { switch (fm) { case flush_get: if (qfirst && last_ts.tv_sec - qfirst->tm.ts.tv_sec >= max_sec) { dfprintf(2, "response_time: flush_get old %p, new %p", flushed_obj, qfirst); if (flushed_obj) xfree(flushed_obj); flushed_obj = qfirst; qfirst = flushed_obj->next; if (qfirst) qfirst->prev = 0; if (flushed_obj == qlast) qlast = 0; hash_remove(flushed_obj, theHash); num_queries--; return &flushed_obj->m; } break; case flush_on: dfprintf(2, "response_time: flush_on %p", flushed_obj); flushing = 1; break; case flush_off: dfprintf(2, "response_time: flush_off %p", flushed_obj); if (flushed_obj) { xfree(flushed_obj); flushed_obj = 0; } flushing = 0; break; default: break; } return 0; }