/* Pi-hole: A black hole for Internet advertisements * (c) 2017 Pi-hole, LLC (https://pi-hole.net) * Network-wide ad blocking via your own hardware. * * FTL Engine * dnsmasq interfacing routines * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ #define FTLDNS #include "dnsmasq/dnsmasq.h" #undef __USE_XOPEN #include "FTL.h" #include "enums.h" #include "dnsmasq_interface.h" #include "shmem.h" #include "overTime.h" #include "database/common.h" #include "database/database-thread.h" #include "datastructure.h" #include "database/gravity-db.h" #include "config/setupVars.h" #include "daemon.h" #include "timers.h" #include "gc.h" #include "regex_r.h" #include "config/config.h" #include "capabilities.h" #include "resolve.h" #include "files.h" // add_to_fifo_buffer() u.a. #include "log.h" // global variable daemonmode #include "args.h" // handle_realtime_signals() #include "signals.h" // atomic_flag_test_and_set() #include // Eventqueue routines #include "events.h" #include // offsetof() #include // logg_rate_limit_message() #include "database/message-table.h" // http_init(), webserver_thread() #include "webserver/webserver.h" // init_memory_database() #include "database/query-table.h" // reread_config() #include "config/config.h" // FTL_fork_and_bind_sockets() #include "main.h" // ntp_server_start() #include "ntp/ntp.h" // get_process_name() #include "procps.h" // init_api_sessions() #include "api/api.h" // Public prototypes (defined in this file, called from other translation units) void FTL_dump_cache_stats(void); // Private prototypes static void print_flags(const unsigned int flags); #define query_set_reply(flags, reply, addr, query, now) _query_set_reply(flags, reply, addr, query, now, __FILE__, __LINE__) static void _query_set_reply(const unsigned int flags, const enum reply_type reply, const union all_addr *addr, queriesData *query, const double now, const char *file, const int line); static bool FTL_check_blocking(const char *domainstr, queriesData *query, clientsData *client, domainsData *domain, DNSCacheData *dns_cache); static void query_blocked(queriesData *query, domainsData *domain, clientsData *client, const enum query_status new_status); static void FTL_forwarded(const unsigned int flags, const char *name, const union all_addr *addr, unsigned short port, const int id, const char *file, const int line); static void FTL_reply(const unsigned int flags, const char *name, const union all_addr *addr, const char *arg, const int id, const char *file, const int line); static void FTL_upstream_error(const union all_addr *addr, const unsigned int flags, const int id, const char *file, const int line); static void FTL_dnssec(const char *result, const union all_addr *addr, const int id, const char *file, const int line); static void mysockaddr_extract_ip_port(const union mysockaddr *server, char ip[ADDRSTRLEN+1], in_port_t *port); static void alladdr_extract_ip(union all_addr *addr, const sa_family_t family, char ip[ADDRSTRLEN+1]); static void check_pihole_PTR(char *domain); #define query_set_dnssec(query, dnssec) _query_set_dnssec(query, dnssec, __FILE__, __LINE__) static void _query_set_dnssec(queriesData *query, const enum dnssec_status dnssec, const char *file, const int line); static char *get_ptrname(const struct in_addr *addr); static const char *check_dnsmasq_name(const char *name); static void get_rcode(const unsigned short rcode, const char **rcodestr, enum reply_type *reply); // Static blocking metadata static bool aabit = false, adbit = false, rabit = false; static const char *blockingreason = ""; static enum reply_type force_next_DNS_reply = REPLY_UNKNOWN; static enum query_status cacheStatus = QUERY_UNKNOWN; static int last_regex_idx = -1; static char *pihole_suffix = NULL; static char *hostname_suffix = NULL; static size_t pihole_suffix_len = 0; static size_t hostname_suffix_len = 0; static size_t hostname_len = 0; static const char *cname_target = NULL; // FTL DNS cache hit/miss counters. // A "hit" means the (domain, client, query-type) tuple was already in FTL's // cache so gravity.db was not queried at all. A "miss" triggers a full set of // gravity/denylist/allowlist lookups. Reset by FTL_dump_cache_stats(). static uint64_t ftl_cache_hits = 0; static uint64_t ftl_cache_misses = 0; // Hot-path performance statistics (gated by debug.performance). // Tracks per-component latency within FTL_new_query() and FTL_reply(). // All updates happen under the SHM lock, so no atomics are needed. // Counters are reset each time FTL_dump_cache_stats() is called (every 5 min). #define PERF_STAT_NEW_QUERY 0 // Total FTL_new_query() under SHM lock #define PERF_STAT_FIND_CLIENT 1 // findClientID() hash lookup #define PERF_STAT_FIND_DOMAIN 2 // findDomainID() hash lookup #define PERF_STAT_CHECK_BLOCKING 3 // FTL_check_blocking() from FTL_new_query (primary path) #define PERF_STAT_REPLY 4 // FTL_reply() under SHM lock // The two sub-slots below only fire on the cache-miss path inside // check_blocking (i.e. a domain/client combination that FTL has not // yet classified). They let the 5-minute rollup localize rare // multi-millisecond outliers to either the allowlist or the // denylist/gravity stage without per-call log spam. They fire for // BOTH the primary and the CNAME-side callers of FTL_check_blocking, // so their call counts cover the full workload while the two parent // slots above/below split primary vs CNAME amplification. #define PERF_STAT_CB_ALLOWLIST 5 // in_allowlist() + in_regex(ALLOW) #define PERF_STAT_CB_DENYLIST 6 // check_domain_blocked() (primary + _esni fallback) #define PERF_STAT_CB_CNAME 7 // FTL_check_blocking() from FTL_CNAME (per-hop) // The three sub-slots below nest inside CB_DENYLIST and break // check_domain_blocked() into its three disjoint engines. Each one // has a different fix domain if it turns out to own the tail: // CDB_EXACT — small SQLite prepared-statement path // CDB_GRAVITY — large gravity/antigravity SQLite probe under mmap // CDB_REGEX — TRE regex engine, per-client #define PERF_STAT_CDB_EXACT 8 // in_denylist() exact-match #define PERF_STAT_CDB_GRAVITY 9 // in_gravity() antigravity + gravity #define PERF_STAT_CDB_REGEX 10 // in_regex(REGEX_DENY) #define PERF_STAT_COUNT 11 static struct { uint64_t calls; // number of invocations uint64_t total_us; // cumulative microseconds uint64_t max_us; // single-call maximum in us uint64_t slow; // calls that took more than 1 ms } query_perf[PERF_STAT_COUNT]; // Start a performance measurement. Declares and fills ts_var with the // current CLOCK_MONOTONIC time. No-op when debug.performance is disabled. #define PERF_START(ts_var) \ struct timespec ts_var = {0}; \ if(config.debug.performance.v.b) \ clock_gettime(CLOCK_MONOTONIC, &(ts_var)) // End a performance measurement and accumulate into the given slot. // No-op when debug.performance is disabled. #define PERF_END(ts_var, slot) \ if(config.debug.performance.v.b) { \ struct timespec _pe; \ clock_gettime(CLOCK_MONOTONIC, &_pe); \ const int64_t _ns = (int64_t)(_pe.tv_sec - (ts_var).tv_sec) * 1000000000LL \ + (int64_t)(_pe.tv_nsec - (ts_var).tv_nsec); \ const uint64_t _us = (uint64_t)(_ns / 1000); \ query_perf[(slot)].calls++; \ query_perf[(slot)].total_us += _us; \ if(_us > query_perf[(slot)].max_us) query_perf[(slot)].max_us = _us; \ if(_us > 1000u) query_perf[(slot)].slow++; \ } #define HOSTNAME "Pi-hole hostname" // Seconds before a MAC lookup that found nothing is repeated. dnsmasq answers // it from its negative ARP cache record for 90 s anyway (src/dnsmasq/arp.c) #define MAC_LOOKUP_RETRY 90u // Fork-private copy of the interface data the most recent query came from static struct { bool haveIPv4; bool haveIPv6; char name[IFNAMSIZ]; union all_addr addr4; union all_addr addr6; } next_iface = {false, false, "", {}, {}}; // Fork-private copy of the server data the most recent reply came from static union mysockaddr last_server = {}; void FTL_dump_cache_stats(void) { const uint64_t total = ftl_cache_hits + ftl_cache_misses; if(total == 0) { log_debug(DEBUG_PERFORMANCE, "FTL cache stats: no queries in last 5 minutes"); } else { log_debug(DEBUG_PERFORMANCE, "FTL cache stats: %"PRIu64" queries, " "%"PRIu64" hits (%.1f%%), %"PRIu64" misses (%.1f%%)", total, ftl_cache_hits, 100.0 * (double)ftl_cache_hits / (double)total, ftl_cache_misses, 100.0 * (double)ftl_cache_misses / (double)total); } // Reset counters for the next 5-minute window ftl_cache_hits = 0; ftl_cache_misses = 0; // Dump per-component hot-path latency statistics static const char * const perf_names[PERF_STAT_COUNT] = { "new_query (total under lock)", "findClientID", "findDomainID", "check_blocking (primary path)", "reply (total under lock)", "check_blocking -> allowlist (miss path)", "check_blocking -> denylist/gravity (miss path)", "check_blocking (CNAME path, per-hop)", "check_domain_blocked -> exact denylist", "check_domain_blocked -> gravity+antigravity", "check_domain_blocked -> regex denylist", }; for(unsigned int i = 0; i < PERF_STAT_COUNT; i++) { if(query_perf[i].calls == 0) { log_debug(DEBUG_PERFORMANCE, "Query perf [%s]: no calls in last 5 minutes", perf_names[i]); continue; } log_debug(DEBUG_PERFORMANCE, "Query perf [%s]: %"PRIu64" calls, " "avg %.1f us, max %.1f us, " "%"PRIu64" slow (>1ms, %.1f%%)", perf_names[i], query_perf[i].calls, (double)query_perf[i].total_us / (double)query_perf[i].calls, (double)query_perf[i].max_us, query_perf[i].slow, 100.0 * (double)query_perf[i].slow / (double)query_perf[i].calls); } // Reset counters for the next 5-minute window memset(query_perf, 0, sizeof(query_perf)); } const char *flagnames[] = {"F_IMMORTAL ", "F_NAMEP ", "F_REVERSE ", "F_FORWARD ", "F_DHCP ", "F_NEG ", "F_HOSTS ", "F_IPV4 ", "F_IPV6 ", "F_BIGNAME ", "F_NXDOMAIN ", "F_CNAME ", "F_DNSKEY ", "F_CONFIG ", "F_DS ", "F_DNSSECOK ", "F_UPSTREAM ", "F_RRNAME ", "F_SERVER ", "F_QUERY ", "F_NOERR ", "F_AUTH ", "F_DNSSEC ", "F_KEYTAG ", "F_SECSTAT ", "F_NO_RR ", "F_IPSET ", "F_NOEXTRA ", "F_DOMAINSRV", "F_RCODE", "F_RR", "F_STALE" }; void FTL_hook(unsigned int flags, const char *name, const union all_addr *addr, char *arg, int id, unsigned short type, const char *file, const int line) { // Extract filename from path const char *path = short_path(file); if(config.debug.flags.v.b) { const char *types = (flags & F_RR) ? querystr(arg, type) : "?"; log_debug(DEBUG_FLAGS, "Processing FTL hook from %s:%d (type: %s, name: \"%s\", id: %i)...", path, line, types, name, id); print_flags(flags); } // The query ID may be negative if this is a TCP query if(id < 0) id = -id; // Check domain name received from dnsmasq name = check_dnsmasq_name(name); // Note: The order matters here! if((flags & F_QUERY) && (flags & F_FORWARD)) ; // New query, handled by FTL_new_query via separate call else if(flags & F_FORWARD && flags & F_SERVER) // forwarded upstream (type is used to store the upstream port) FTL_forwarded(flags, name, addr, type, id, path, line); else if(flags == F_SECSTAT) // DNSSEC validation result FTL_dnssec(arg, addr, id, path, line); else if(flags & F_RCODE && !(flags & F_CONFIG) && name && strcasecmp(name, "error") == 0) // upstream sent something different than NOERROR or NXDOMAIN FTL_upstream_error(addr, flags, id, path, line); else if(flags & F_NOEXTRA && flags & F_DNSSEC) { // This is a new DNSSEC query (dnssec-query[DS]) if(!config.dns.showDNSSEC.v.b) return; // Type is overloaded with port since 2d65d55, so we have to // derive the real query type from the arg string unsigned short qtype = type; if(strcmp(arg, "dnssec-query[DNSKEY]") == 0) { qtype = T_DNSKEY; arg = (char*)"dnssec-query"; } else if(strcmp(arg, "dnssec-query[DS]") == 0) { qtype = T_DS; arg = (char*)"dnssec-query"; } else if(strcmp(arg, "dnssec-retry[DNSKEY]") == 0) { qtype = T_DNSKEY; arg = (char*)"dnssec-retry"; } else if(strcmp(arg, "dnssec-retry[DS]") == 0) { qtype = T_DS; arg = (char*)"dnssec-retry"; } else { arg = (char*)"dnssec-unknown"; } _FTL_new_query(flags, name, NULL, arg, qtype, id, INTERNAL, file, line); // forwarded upstream (type is used to store the upstream port) FTL_forwarded(flags, name, addr, type, id, path, line); } else if(flags & F_AUTH) ; // Ignored else if(flags & F_IPSET) ; // Ignored else if(flags == F_UPSTREAM && strcmp(arg, "truncated") == 0) ; // Ignored - truncated reply // // flags will by (F_UPSTREAM | F_NOEXTRA) with type being // T_DNSKEY or T_DS when this is a truncated DNSSEC reply // // otherwise, flags will be F_UPSTREAM and the type is not set // (== 0) else FTL_reply(flags, name, addr, arg, id, path, line); } // The blocking reason, the CNAME target, the forced reply, the redirecting // regex and the cache status describe one query, so they are dropped on every // way out of _FTL_make_answer() below and when the next query arrives static void unset_blocking_metadata(void) { blockingreason = ""; cname_target = NULL; force_next_DNS_reply = REPLY_UNKNOWN; last_regex_idx = -1; cacheStatus = QUERY_UNKNOWN; } // This is inspired by make_local_answer() size_t _FTL_make_answer(struct dns_header *header, char *limit, const size_t len, unsigned char ede_data[MAX_EDE_DATA], size_t *ede_len, const char *file, const int line) { log_debug(DEBUG_FLAGS, "FTL_make_answer() called from %s:%d", short_path(file), line); // Exit early if there are no questions in this query if(ntohs(header->qdcount) == 0) { unset_blocking_metadata(); return 0; } // Get question name char name[MAXDNAMESTR + 1] = { 0 }; unsigned char *p = (unsigned char *)(header+1); if (!extract_name(header, len, &p, name, 1, 4)) { unset_blocking_metadata(); return 0; } // Debug logging log_debug(DEBUG_QUERIES, "Preparing reply for \"%s\"", name); // Get question type int qtype, flags = 0; GETSHORT(qtype, p); // Set flags based on what we will reply with if(qtype == T_A) flags = F_IPV4; // A type else if(qtype == T_AAAA) flags = F_IPV6; // AAAA type else if(qtype == T_ANY) flags = F_IPV4 | F_IPV6; // ANY type else flags = F_NOERR; // empty record // Prepare answer records bool forced_ip = false; // Check first if we need to force our reply to something different than the // default/configured blocking mode. For instance, we need to force NXDOMAIN // for intercepted _esni.* queries or the Mozilla canary domain. if(force_next_DNS_reply == REPLY_NXDOMAIN) { flags = F_NXDOMAIN; // Reset DNS reply forcing force_next_DNS_reply = REPLY_UNKNOWN; // Debug logging log_debug(DEBUG_QUERIES, "Forced DNS reply to NXDOMAIN"); } else if(force_next_DNS_reply == REPLY_NODATA) { flags = F_NOERR; // Reset DNS reply forcing force_next_DNS_reply = REPLY_UNKNOWN; // Debug logging log_debug(DEBUG_QUERIES, "Forced DNS reply to NODATA"); } else if(force_next_DNS_reply == REPLY_REFUSED) { // Empty flags result in REFUSED flags = 0; // Reset DNS reply forcing force_next_DNS_reply = REPLY_UNKNOWN; // Debug logging log_debug(DEBUG_QUERIES, "Forced DNS reply to REFUSED"); } else if(force_next_DNS_reply == REPLY_IP) { // We do not need to change the flags here, // they are already properly set (F_IPV4 and/or F_IPV6) forced_ip = true; // Reset DNS reply forcing force_next_DNS_reply = REPLY_UNKNOWN; // Debug logging log_debug(DEBUG_QUERIES, "Forced DNS reply to IP"); } else if(force_next_DNS_reply == REPLY_NONE) { // Reset DNS reply forcing force_next_DNS_reply = REPLY_UNKNOWN; // Debug logging log_debug(DEBUG_QUERIES, "Forced DNS reply to NONE - dropping this query"); unset_blocking_metadata(); return 0; } else { // Overwrite flags only if not replying with a forced reply if(config.dns.blocking.mode.v.blocking_mode == MODE_NX) { // If we block in NXDOMAIN mode, we set flags to NXDOMAIN // (NEG will be added after setup_reply() below) flags = F_NXDOMAIN; log_debug(DEBUG_QUERIES, "Configured blocking mode is NXDOMAIN"); } else if(config.dns.blocking.mode.v.blocking_mode == MODE_NODATA || (config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA && (flags & F_IPV6))) { // If we block in NODATA mode or NODATA for AAAA queries, we apply // the NOERROR response flag. This ensures we're sending an empty response flags = F_NOERR; log_debug(DEBUG_QUERIES, "Configured blocking mode is NODATA%s", config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA ? "-IPv6" : ""); } } // Check for regex redirecting bool redirecting = false; union all_addr redirect_addr4 = {}, redirect_addr6 = {}; if(last_regex_idx > -1) { redirecting = regex_get_redirect(last_regex_idx, &redirect_addr4.addr4, &redirect_addr6.addr6); // Reset regex redirection forcing last_regex_idx = -1; // Debug logging log_debug(DEBUG_QUERIES, "Regex match is %sredirected", redirecting ? "" : "NOT "); } if(force_next_DNS_reply == REPLY_CNAME && cname_target != NULL) { // Set flags to CNAME reply flags = F_CONFIG | F_CNAME; // Add A record (if available) if(redirect_addr4.addr4.s_addr != 0) flags |= F_IPV4; // Add AAAA record (if available) if(!IN6_IS_ADDR_UNSPECIFIED(&redirect_addr6.addr6)) flags |= F_IPV6; // Reset DNS reply forcing force_next_DNS_reply = REPLY_UNKNOWN; } // Derive EDE code and text from cacheStatus int ede_code = EDE_UNSET; const char *ede_text = NULL; switch(cacheStatus) { case QUERY_UNKNOWN: // case QUERY_CACHE: case QUERY_FORWARDED: case QUERY_RETRIED: case QUERY_RETRIED_DNSSEC: case QUERY_IN_PROGRESS: case QUERY_DBBUSY: case QUERY_CACHE_STALE: case QUERY_STATUS_MAX: // Not going through this function break; case QUERY_GRAVITY: ede_code = EDE_BLOCKED; ede_text = "gravity"; break; case QUERY_GRAVITY_CNAME: ede_code = EDE_BLOCKED; ede_text = "gravity (CNAME)"; break; case QUERY_DENYLIST: ede_code = EDE_BLOCKED; ede_text = "denylist"; break; case QUERY_DENYLIST_CNAME: ede_code = EDE_BLOCKED; ede_text = "denylist (CNAME)"; break; case QUERY_REGEX: ede_code = EDE_BLOCKED; ede_text = "regex"; break; case QUERY_REGEX_CNAME: ede_code = EDE_BLOCKED; ede_text = "regex (CNAME)"; break; case QUERY_SPECIAL_DOMAIN: ede_code = EDE_BLOCKED; ede_text = "special"; break; case QUERY_EXTERNAL_BLOCKED_NXRA: ede_code = EDE_BLOCKED; ede_text = "upstream NXRA"; break; case QUERY_EXTERNAL_BLOCKED_NULL: ede_code = EDE_BLOCKED; ede_text = "upstream NULL"; break; case QUERY_EXTERNAL_BLOCKED_IP: ede_code = EDE_BLOCKED; ede_text = "upstream IP"; break; case QUERY_EXTERNAL_BLOCKED_EDE15: ede_code = EDE_BLOCKED; ede_text = "upstream EDE 15"; break; case QUERY_CACHE: ede_code = EDE_SYNTHESIZED; ede_text = "synthesized"; break; } // Reset global DNS cache status cacheStatus = QUERY_UNKNOWN; // Debug logging log_debug(DEBUG_QUERIES, "Setting EDE: %s (%d) + \"%s\"", ede_code != EDE_UNSET ? edestr(ede_code) : "---", ede_code, ede_text ? ede_text : "---"); if(ede_code != EDE_UNSET && config.dns.blocking.edns.v.edns_mode > EDNS_MODE_NONE) { // Set EDE INFO-CODE (network byte order) uint16_t swap = htons(ede_code); memcpy(ede_data, &swap, sizeof(swap)); *ede_len = sizeof(swap); // Set EDE INFO-TEXT (if available) if(ede_text && config.dns.blocking.edns.v.edns_mode > EDNS_MODE_CODE) { size_t extra_len = strlen(ede_text); // Truncate if necessary if(extra_len > MAX_EDE_DATA - *ede_len) extra_len = MAX_EDE_DATA - *ede_len; memcpy(ede_data + *ede_len, ede_text, extra_len); *ede_len += extra_len; } } // Debug logging print_flags(flags); // Setup reply header setup_reply(header, flags, ede_code); // Add NEG flag when replying with NXDOMAIN or NODATA. This is necessary // to get proper logging in pihole.log At the same time, we cannot add // NEG before calling setup_reply() as it would, otherwise, result in an // incorrect "nowhere to forward to" log entry (because setup_reply() // checks for equality of flags instead of doing a bitmask comparison). if(flags == F_NXDOMAIN || flags == F_NOERR) flags |= F_NEG; // Add flags according to current blocking mode // Set blocking_flags to F_HOSTS so dnsmasq logs blocked queries being answered from a specific source // (it would otherwise assume it knew the blocking status from cache which would prevent us from // printing the blocking source (blacklist, regex, gravity) in dnsmasq's log file, our pihole.log) if(flags != 0) flags |= F_HOSTS; // Skip questions so we can start adding answers (if applicable) if (!(p = skip_questions(header, len))) { unset_blocking_metadata(); return 0; } // Are we replying to pi.hole / / pi.hole. / . ? const bool hostn = strcmp(blockingreason, HOSTNAME) == 0; int trunc = 0; // Add CNAME answer record if requested if(flags & F_CNAME) { // Debug logging if(config.debug.queries.v.b) log_debug(DEBUG_QUERIES, " Adding RR: \"%s CNAME %s\"", name, cname_target); // Add CNAME resource record header->ancount = htons(ntohs(header->ancount) + 1); if(add_resource_record(header, limit, &trunc, sizeof(struct dns_header), &p, daemon->local_ttl, NULL, T_CNAME, C_IN, (char*)"d", cname_target)) log_query(flags, name, NULL, (char*)blockingreason, 0); } // Add A answer record if requested if(flags & F_IPV4) { union all_addr addr = {}; // Overwrite with IP address if requested if(redirecting) { log_debug(DEBUG_QUERIES, "Using regex redirected A address"); memcpy(&addr, &redirect_addr4, sizeof(addr)); } else if(config.dns.blocking.mode.v.blocking_mode == MODE_IP || config.dns.blocking.mode.v.blocking_mode == MODE_IP_NODATA_AAAA || forced_ip) { if(hostn && config.dns.reply.host.force4.v.b) { log_debug(DEBUG_QUERIES, "Using dns.reply.host.force4"); memcpy(&addr, &config.dns.reply.host.v4.v.in_addr, sizeof(addr.addr4)); } else if(!hostn && config.dns.reply.blocking.force4.v.b) { log_debug(DEBUG_QUERIES, "Using dns.reply.blocking.force4"); memcpy(&addr, &config.dns.reply.blocking.v4.v.in_addr, sizeof(addr.addr4)); } else { log_debug(DEBUG_QUERIES, "Using next_iface A address"); memcpy(&addr, &next_iface.addr4, sizeof(addr.addr4)); } } // Debug logging if(config.debug.queries.v.b) { char ip[ADDRSTRLEN+1] = { 0 }; alladdr_extract_ip(&addr, AF_INET, ip); log_debug(DEBUG_QUERIES, " Adding RR: \"%s A %s\"", name, ip); } // Add A resource record header->ancount = htons(ntohs(header->ancount) + 1); if(add_resource_record(header, limit, &trunc, sizeof(struct dns_header), &p, hostn ? daemon->local_ttl : config.dns.blockTTL.v.ui, NULL, T_A, C_IN, (char*)"4", &addr.addr4)) log_query(flags & ~F_IPV6, name, &addr, (char*)blockingreason, 0); } // Add AAAA answer record if requested if(flags & F_IPV6) { union all_addr addr = {}; // Overwrite with IP address if requested if(redirecting) { log_debug(DEBUG_QUERIES, "Using regex redirected AAAA address"); memcpy(&addr, &redirect_addr6, sizeof(addr)); } else if(config.dns.blocking.mode.v.blocking_mode == MODE_IP || forced_ip) { if(hostn && config.dns.reply.host.force6.v.b) { log_debug(DEBUG_QUERIES, "Using dns.reply.host.force6"); memcpy(&addr, &config.dns.reply.host.v6.v.in6_addr, sizeof(addr.addr6)); } else if(!hostn && config.dns.reply.blocking.force6.v.b) { log_debug(DEBUG_QUERIES, "Using dns.reply.blocking.force6"); memcpy(&addr, &config.dns.reply.blocking.v6.v.in6_addr, sizeof(addr.addr6)); } else { log_debug(DEBUG_QUERIES, "Using next_iface AAAA address"); memcpy(&addr, &next_iface.addr6, sizeof(addr.addr6)); } } // Debug logging if(config.debug.queries.v.b) { char ip[ADDRSTRLEN+1] = { 0 }; alladdr_extract_ip(&addr, AF_INET6, ip); log_debug(DEBUG_QUERIES, " Adding RR: \"%s AAAA %s\"", name, ip); } // Add AAAA resource record header->ancount = htons(ntohs(header->ancount) + 1); if(add_resource_record(header, limit, &trunc, sizeof(struct dns_header), &p, hostn ? daemon->local_ttl : config.dns.blockTTL.v.ui, NULL, T_AAAA, C_IN, (char*)"6", &addr.addr6)) log_query(flags & ~F_IPV4, name, &addr, (char*)blockingreason, 0); } // Log empty replies if(!(flags & (F_IPV4 | F_IPV6))) { if(flags == 0) { // REFUSED union all_addr addr = {}; addr.log.rcode = REFUSED; addr.log.ede = EDE_BLOCKED; log_query(F_RCODE | F_HOSTS, name, &addr, (char*)blockingreason, 0); } else { // NODATA/NXDOMAIN // gravity blocked abc.com is NODATA/NXDOMAIN log_query(flags, name, NULL, (char*)blockingreason, 0); } } // Indicate if truncated (client should retry over TCP) if (trunc) header->hb3 |= HB3_TC; unset_blocking_metadata(); return p - (unsigned char *)header; } static bool is_pihole_domain(const char *domain) { // Cache plain hostname length on first call (independent of // domain_suffix) if(hostname_len == 0) hostname_len = strlen(hostname()); // Build "pi.hole." domain if not already built and if // domain suffix is configured if(!pihole_suffix && daemon->domain_suffix) { pihole_suffix = calloc(strlen(daemon->domain_suffix) + 9, sizeof(char)); strcpy(pihole_suffix, "pi.hole."); strcat(pihole_suffix, daemon->domain_suffix); pihole_suffix_len = strlen(pihole_suffix); log_debug(DEBUG_QUERIES, "Domain suffix is \"%s\"", daemon->domain_suffix); } // Build "." domain if not already built and if // domain suffix is configured if(!hostname_suffix && daemon->domain_suffix) { hostname_suffix = calloc(strlen(hostname()) + strlen(daemon->domain_suffix) + 2, sizeof(char)); strcpy(hostname_suffix, hostname()); strcat(hostname_suffix, "."); strcat(hostname_suffix, daemon->domain_suffix); hostname_suffix_len = strlen(hostname_suffix); } // Pre-filter with integer length comparison before any strcasecmp: // in the common case (domain is not pi.hole/hostname), all four checks // short-circuit after a single strlen + four integer comparisons. const size_t dlen = strlen(domain); return (dlen == 7u && strcasecmp(domain, "pi.hole") == 0) || (dlen == hostname_len && strcasecmp(domain, hostname()) == 0) || (pihole_suffix && dlen == pihole_suffix_len && strcasecmp(domain, pihole_suffix) == 0) || (hostname_suffix && dlen == hostname_suffix_len && strcasecmp(domain, hostname_suffix) == 0); } bool _FTL_new_query(const unsigned int flags, const char *name, union mysockaddr *addr, char *arg, const unsigned short qtype, int id, enum protocol proto, const char *file, const int line) { // Create new query in data structure // Get timestamp const double querytimestamp = double_time(); // Determine query type via direct-mapped lookup table // DNS wire types are 16-bit but standard types fit in [0,255]. // Uninitialized entries are 0; since all known mappings are non-zero, a // zero result means either TYPE_NONE (qtype==0) or an unknown type // (TYPE_OTHER). static const uint8_t qtype_map[256] = { [T_A] = TYPE_A, [T_NS] = TYPE_NS, [T_SOA] = TYPE_SOA, [T_PTR] = TYPE_PTR, [T_MX] = TYPE_MX, [T_TXT] = TYPE_TXT, [T_AAAA] = TYPE_AAAA, [T_SRV] = TYPE_SRV, [T_NAPTR] = TYPE_NAPTR, [T_DS] = TYPE_DS, [T_RRSIG] = TYPE_RRSIG, [T_DNSKEY] = TYPE_DNSKEY, [64] = TYPE_SVCB, // draft-ietf-dnsop-svcb-https [65] = TYPE_HTTPS, // draft-ietf-dnsop-svcb-https [T_ANY] = TYPE_ANY, }; enum query_type querytype; if(qtype == 0) querytype = TYPE_NONE; else if(qtype < 256 && qtype_map[qtype]) querytype = (enum query_type)qtype_map[qtype]; else querytype = TYPE_OTHER; // Check domain name received from dnsmasq name = check_dnsmasq_name(name); // Not every query that sets up an answer gets one through // _FTL_make_answer(), so start every client query from a clean state if(proto != INTERNAL) unset_blocking_metadata(); // If domain is "pi.hole" or the local hostname we skip analyzing this query // and, instead, immediately reply with the IP address - these queries are not further analyzed if(querytype != TYPE_NONE && is_pihole_domain(name)) { if(querytype == TYPE_A || querytype == TYPE_AAAA || querytype == TYPE_ANY) { // "Block" this query by sending the interface IP address // Send NODATA when the current interface doesn't have // the requested IP address, for instance AAAA on an // virtual interface that has only an IPv4 address if((querytype == TYPE_A && !next_iface.haveIPv4 && !config.dns.reply.host.force4.v.b) || (querytype == TYPE_AAAA && !next_iface.haveIPv6 && !config.dns.reply.host.force6.v.b)) force_next_DNS_reply = REPLY_NODATA; else force_next_DNS_reply = REPLY_IP; blockingreason = HOSTNAME; log_debug(DEBUG_QUERIES, "Replying to %s with %s", name, force_next_DNS_reply == REPLY_IP ? "interface-local IP address" : "NODATA due to missing iface address"); cacheStatus = QUERY_CACHE; return true; } else { // Don't block this query return false; } } // Check if this is a PTR request for a local interface. // If so, we inject a "pi.hole" reply here if(querytype == TYPE_PTR && config.dns.piholePTR.v.ptr_type != PTR_NONE) check_pihole_PTR((char*)name); // Convert domain to lower case (single-pass copy + lowercase) char domainString[MAXDOMAINLEN]; strcpy_tolower(domainString, name, sizeof(domainString)); // Get client IP address // The requestor's IP address can be rewritten using EDNS(0) client // subnet (ECS) data), however, we do not rewrite the IPs ::1 and // 127.0.0.1 to avoid queries originating from localhost of the // *distant* machine as queries coming from the *local* machine const sa_family_t family = addr ? addr->sa.sa_family : AF_INET; in_port_t clientPort = daemon->port; bool internal_query = false; char clientIP[ADDRSTRLEN+1] = { 0 }; // Set when clientIP comes from EDNS(0) rather than the packet source bool edns_client = false; // Set when clientIP comes from ECS, an address behind the forwarder bool ecs_client = false; ednsData *edns = getEDNS(id); if(config.dns.EDNS0ECS.v.b && edns && edns->client_set) { // Use ECS provided client strncpy(clientIP, edns->client, ADDRSTRLEN); clientIP[ADDRSTRLEN] = '\0'; edns_client = true; ecs_client = true; } else if(addr) { // Use original requestor mysockaddr_extract_ip_port(addr, clientIP, &clientPort); } else { // No client address available, this is an automatically generated (e.g. // DNSSEC) query internal_query = true; strcpy(clientIP, "::"); } // Check if user wants to skip queries coming from localhost if(config.dns.ignoreLocalhost.v.b && (strcmp(clientIP, "127.0.0.1") == 0 || strcmp(clientIP, "::1") == 0)) return false; // Lock shared memory lock_shm(); PERF_START(_pnq); const int queryID = counters->queries; // Find client IP PERF_START(_pfc); const int clientID = findClientID(clientIP, true, false, querytimestamp); PERF_END(_pfc, PERF_STAT_FIND_CLIENT); // Get client pointer clientsData *client = getClient(clientID, true); if(client == NULL) { // Encountered memory error, skip query // Release thread lock unlock_shm(); return false; } // Update rolling window of queries per second update_qps(querytimestamp); // Interface name is only available for regular queries, not for // automatically generated DNSSEC queries const char *interface = internal_query ? "-" : next_iface.name; // Check rate-limit for this client if(!internal_query && config.dns.rateLimit.count.v.ui > 0 && (++client->rate_limit > config.dns.rateLimit.count.v.ui || client->flags.rate_limited)) { // Log the first rate-limited query for this client in this // interval, after the lock is released below: the message goes // to pihole-FTL.db, and a contended database can hold us in // sqliteBusyCallback() for up to DATABASE_BUSY_TIMEOUT, which // would stall every DNS query and API worker waiting on the // lock. We do not log the blocked domain for privacy reasons unsigned int rate_limit_count = 0; if(!client->flags.rate_limited) { rate_limit_count = client->rate_limit; // Reset rate-limiting counter so we can count what // comes within the adjacent interval client->rate_limit = 0; } // Memorize this client needs rate-limiting client->flags.rate_limited = true; // Block this query force_next_DNS_reply = REPLY_REFUSED; blockingreason = "Rate-limiting"; // Do not further process this query, Pi-hole has never seen it. // Undo the client count increment from findClientID() above: // no query record is created for rate-limited queries, so GC // will never decrement this counter — leaving it permanently // inflated for the lifetime of the process. change_clientcount(client, -1, 0, -1, 0); unlock_shm(); // clientIP is a local buffer, so it stays valid here if(rate_limit_count > 0) logg_rate_limit_message(clientIP, rate_limit_count); return true; } // The query ID is negative if this is a TCP query if(id < 0) { id = -id; // Safety check: If the query ID is negative, the protocol // should be TCP if(proto != TCP) { proto = TCP; log_debug(DEBUG_ANY, "Query %d has negative ID, but protocol is not TCP", id); } } // Log new query if in debug mode if(config.debug.queries.v.b) { const char *types = querystr(arg, qtype); log_debug(DEBUG_QUERIES, "**** new %sIPv%d %s%s \"%s\" from %s/%s#%d (ID %i, FTL %i, %s:%i)", proto == TCP ? "TCP " : proto == UDP ? "UDP " : "", family == AF_INET ? 4 : 6, types, querytype == TYPE_NONE ? "" : " query", name, interface, internal_query ? "" : clientIP, clientPort, id, queryID, short_path(file), line); } // Update overTime const unsigned int timeidx = getOverTimeID(querytimestamp); // Skip rest of the analysis if this query is not of type A or AAAA // but user wants to see only A and AAAA queries (pre-v4.1 behavior) if(config.dns.analyzeOnlyAandAAAA.v.b && querytype != TYPE_A && querytype != TYPE_AAAA) { // Don't process this query further here if(config.debug.queries.v.b) log_debug(DEBUG_QUERIES, "Skipping new query (%i)", id); // Undo the findClientID() increment, as for the rate-limited // branch above: no query record is created here change_clientcount(client, -1, 0, -1, 0); unlock_shm(); return false; } // Go through already knows domains and see if it is one of them PERF_START(_pfd); const int domainID = findDomainID(domainString, true); PERF_END(_pfd, PERF_STAT_FIND_DOMAIN); // Save everything queriesData *query = getQuery(queryID, false); if(query == NULL) { // Encountered memory error, skip query log_err("No memory available, skipping query analysis"); // Release thread lock unlock_shm(); return false; } // Fill query object with available data query->magic = MAGICBYTE; query->timestamp = querytimestamp; query->type = querytype; counters->querytype[querytype]++; log_debug(DEBUG_STATUS, "query type %d set (new query), ID = %d, new count = %u", query->type, id, counters->querytype[query->type]); query->qtype = qtype; query->id = id; // Has to be set before calling query_set_status() queryIDMap_insert(id, queryID); // This query is unknown as long as no reply has been found and analyzed query_set_status_init(query, QUERY_UNKNOWN); query->domainID = domainID; query->clientID = clientID; // Initialize database fields // This query is new and not yet known to the database query->db = -1; query->flags.database.imported = false; query->flags.database.changed = true; query->flags.complete = false; query->flags.upstream_counted = false; query->response = querytimestamp; query->flags.response_calculated = false; // Initialize reply type query->reply = REPLY_UNKNOWN; counters->reply[REPLY_UNKNOWN]++; log_debug(DEBUG_STATUS, "reply type %u set (new query), ID = %d, new count = %u", query->reply, query->id, counters->reply[query->reply]); // Store DNSSEC result for this domain query->dnssec = DNSSEC_UNKNOWN; query->CNAME_domainID = -1; // This query is not yet known ad forwarded or blocked query->flags.blocked = false; query->flags.allowed = false; // Indicator that this query was not forwarded so far query->upstreamID = -1; // Check and apply possible privacy level rules // The currently set privacy level (at the time the query is // generated) is stored in the queries structure query->privacylevel = config.misc.privacylevel.v.privacy_level; // Query extended DNS error query->ede = EDE_UNSET; // Initialize cache ID, may be reusing an existing one if this // (domain,client,type) tuple was already seen before query->cacheID = findCacheID(domainID, clientID, querytype, true); DNSCacheData *dns_cache_entry = query->cacheID > -1 ? getDNSCache(query->cacheID, true) : NULL; if(dns_cache_entry != NULL) dns_cache_entry->refcount++; // Increase DNS queries counter counters->queries++; // Update overTime data structure with the new client change_clientcount(client, 0, 0, timeidx, 1); // Set lastQuery timer and add one query for network table client->lastQuery = querytimestamp; client->numQueriesARP++; // Update domain's last query time domainsData *domain = getDomain(domainID, false); if(domain != NULL) domain->lastQuery = querytimestamp; // Process interface information of client (if available) // Skip interface name length 1 to skip "-". No real interface should // have a name with a length of 1... if(!internal_query && interface[0] != '\0' && interface[1] != '\0') { if(client->ifacepos == 0u) { // Store in the client data if unknown so far client->ifacepos = addstr(interface); } else { // Check if this is still the same interface or // if the client moved to another interface // (may require group re-processing) const char *oldiface = getstr(client->ifacepos); if(strcasecmp(oldiface, interface) != 0) { if(config.debug.clients.v.b) { const char *clientName = getstr(client->namepos); log_debug(DEBUG_CLIENTS, "Client %s (%s) changed interface: %s -> %s", clientIP, clientName, oldiface, interface); } // Adopt the new interface so the in-memory // interface match in get_client_groupids() // re-resolves against it client->ifacepos = addstr(interface); gravityDB_reload_groups(client); } } } // Set client MAC address from EDNS(0) information (if available) if(config.dns.EDNS0ECS.v.b && edns && edns->mac_set) { // If the MAC just became known or changed, clear found_group so // the next query re-resolves this client's groups - the MAC is // one of the identities used for group assignment if(client->hwlen != 6 || memcmp(client->hwaddr, edns->mac_byte, 6) != 0) client->flags.found_group = false; memcpy(client->hwaddr, edns->mac_byte, 6); client->hwlen = 6; } // Try to obtain MAC address from dnsmasq's cache (also asks the kernel) // Don't do this for internally generated queries (e.g., DNSSEC), if the // MAC address is already known or if the netlink socket is not available // (e.g., when retrying a query using TCP after UDP truncation). ECS // clients usually have no neighbor entry here, and a lookup that found // nothing (hwlen 0) is repeated after MAC_LOOKUP_RETRY seconds at most if(!internal_query && !ecs_client && client->hwlen < 1 && daemon->netlinkfd > 0 && (client->hwlen != 0 || ABS_TO_SHM_TIME((time_t)querytimestamp) - client->lastMACLookup >= MAC_LOOKUP_RETRY)) { // find_mac() may trigger a netlink kernel call // (iface_enumerate) to refresh the ARP table on a cache miss. // Release the SHM lock so this potentially slow I/O doesn't // block all other threads (API, database, GC, TCP workers). unlock_shm(); // Look up the address the client is identified by: for an // EDNS(0)-provided client, the packet source is the forwarder union mysockaddr mac_addr = { 0 }; if(edns_client) { if(inet_pton(AF_INET, clientIP, &mac_addr.in.sin_addr) == 1) mac_addr.sa.sa_family = AF_INET; else if(inet_pton(AF_INET6, clientIP, &mac_addr.in6.sin6_addr) == 1) mac_addr.sa.sa_family = AF_INET6; } else if(addr) mac_addr = *addr; unsigned char hwaddr[16] = {0}; const int hwlen = mac_addr.sa.sa_family == AF_UNSPEC ? 0 : find_mac(&mac_addr, hwaddr, 1, time(NULL)); // Reacquire lock and re-fetch client pointer (SHM may have // been remapped while we were unlocked) lock_shm(); client = getClient(clientID, true); // A forked TCP worker only searches the ARP cache it inherited, so // its miss leaves the lookup to the next query of the main process if(client != NULL && (hwlen > 0 || daemon->pipe_to_parent == -1)) { // If a MAC was just learned (this runs only while it was // still unknown), clear found_group so the next query // re-resolves this client's groups using it if(hwlen == 6 && (client->hwlen != 6 || memcmp(client->hwaddr, hwaddr, sizeof(hwaddr)) != 0)) client->flags.found_group = false; memcpy(client->hwaddr, hwaddr, sizeof(hwaddr)); client->hwlen = hwlen; client->lastMACLookup = ABS_TO_SHM_TIME((time_t)querytimestamp); } // Re-fetch all SHM pointers as SHM may have been remapped. The // query is looked up by its dnsmasq ID as its index is relative // to queries_offset and not valid across the unlocked section. // Domains and clients are not offset-indexed and are fine const int new_queryID = findQueryID(id); query = new_queryID < 0 ? NULL : getQuery(new_queryID, true); domain = getDomain(domainID, true); dns_cache_entry = query != NULL && query->cacheID > -1 ? getDNSCache(query->cacheID, true) : NULL; if(config.debug.arp.v.b) { if(hwlen == 6) { log_debug(DEBUG_ARP, "find_mac(\"%s\") returned hardware address " "%02X:%02X:%02X:%02X:%02X:%02X", clientIP, hwaddr[0], hwaddr[1], hwaddr[2], hwaddr[3], hwaddr[4], hwaddr[5]); } else { log_debug(DEBUG_ARP, "find_mac(\"%s\") returned %i bytes of data", clientIP, hwlen); } } } bool blockDomain = false; // Check if this should be blocked only for active queries // (skipped for internally generated ones, e.g., DNSSEC). Guard against // query == NULL, which the getQuery() re-fetch after find_mac() above // may yield if SHM was remapped/recycled while we were unlocked. if(query != NULL && !internal_query && querytype != TYPE_NONE) { PERF_START(_pcb); blockDomain = FTL_check_blocking(domainString, query, client, domain, dns_cache_entry); PERF_END(_pcb, PERF_STAT_CHECK_BLOCKING); } // Store query in database if(query != NULL) query->flags.database.changed = true; PERF_END(_pnq, PERF_STAT_NEW_QUERY); // Release thread lock unlock_shm(); return blockDomain; } void _FTL_iface(struct irec *recviface, const union all_addr *addr, const sa_family_t addrfamily, const char *file, const int line) { // Debug logging log_debug(DEBUG_NETWORKING, "Interfaces: Called from %s:%d", short_path(file), line); // Check if we need to identify the receiving interface by its address // before testing the cache (we need the resolved recviface pointer) if(!recviface && addr && ((addrfamily == AF_INET && addr->addr4.s_addr != INADDR_ANY) || (addrfamily == AF_INET6 && !IN6_IS_ADDR_UNSPECIFIED(&addr->addr6)))) { if(config.debug.networking.v.b) { char addrstr[INET6_ADDRSTRLEN] = { 0 }; if(addrfamily == AF_INET) inet_ntop(AF_INET, &addr->addr4, addrstr, INET6_ADDRSTRLEN); else // if(addrfamily == AF_INET6) inet_ntop(AF_INET6, &addr->addr6, addrstr, INET6_ADDRSTRLEN); log_debug(DEBUG_NETWORKING, "Identifying interface (looking for %s):", addrstr); } // Loop over interfaces and try to find match for (struct irec *iface = daemon->interfaces; iface; iface = iface->next) { char addrstr[INET6_ADDRSTRLEN] = { 0 }; const char *iname = iface->slabel ? iface->slabel : iface->name; if(iface->addr.sa.sa_family == AF_INET) { if(config.debug.networking.v.b) { inet_ntop(AF_INET, &iface->addr.in.sin_addr, addrstr, INET6_ADDRSTRLEN); log_debug(DEBUG_NETWORKING, " - IPv4 interface %s (%d,%d) is %s", iname, iface->index, iface->label, addrstr); } if(iface->addr.in.sin_addr.s_addr == addr->addr4.s_addr) { // Set receiving interface recviface = iface; break; } } else if(iface->addr.sa.sa_family == AF_INET6) { if(config.debug.networking.v.b) { inet_ntop(AF_INET6, &iface->addr.in6.sin6_addr, addrstr, INET6_ADDRSTRLEN); log_debug(DEBUG_NETWORKING, " - IPv6 interface %s (%d,%d) is %s", iname, iface->index, iface->label, addrstr); } if(IN6_ARE_ADDR_EQUAL(&iface->addr.in6.sin6_addr, &addr->addr6)) { // Set receiving interface recviface = iface; break; } } } log_debug(DEBUG_NETWORKING, recviface ? " ^^^^^ MATCH ^^^^^" : " --> NO MATCH <--"); } // Return early when there is no interface available at this point // This means we didn't get one passed + we didn't find one above if(!recviface) { log_debug(DEBUG_NETWORKING, "No receiving interface available at this point"); // Invalidate next_iface since we have no interface memset(&next_iface.addr4, 0, sizeof(next_iface.addr4)); memset(&next_iface.addr6, 0, sizeof(next_iface.addr6)); next_iface.haveIPv4 = next_iface.haveIPv6 = false; next_iface.name[0] = '-'; next_iface.name[1] = '\0'; return; } // Recomputed on every call. dnsmasq keeps one irec per address, so the // irec pointer says nothing about whether the addresses behind it have // changed, and establishing that they have not costs at least as much as // collecting them again: the loop below stops as soon as it has an IPv4 // and a ULA IPv6 address, while any check covering every address cannot // stop early. What it would save is debug-gated anyway memset(&next_iface.addr4, 0, sizeof(next_iface.addr4)); memset(&next_iface.addr6, 0, sizeof(next_iface.addr6)); next_iface.haveIPv4 = next_iface.haveIPv6 = false; next_iface.name[0] = '-'; next_iface.name[1] = '\0'; // Determine addresses of this interface, we have to loop over all interfaces as // recviface will always only contain *either* IPv4 or IPv6 information bool haveGUAv6 = false, haveULAv6 = false; log_debug(DEBUG_NETWORKING, "Analyzing interfaces:"); for (struct irec *iface = daemon->interfaces; iface != NULL; iface = iface->next) { const sa_family_t family = iface->addr.sa.sa_family; const char *iname = iface->slabel ? iface->slabel : iface->name; // If this interface has no name, we skip it if(iname == NULL) { if(config.debug.networking.v.b) log_debug(DEBUG_NETWORKING, " - SKIP IPv%d interface (%d,%d): no name", family == AF_INET ? 4 : 6, iface->index, iface->label); continue; } // Check if this is the interface we want if(iface->index != recviface->index || iface->label != recviface->label) { if(config.debug.networking.v.b) log_debug(DEBUG_NETWORKING, " - SKIP IPv%d interface %s: (%d,%d) != (%d,%d)", family == AF_INET ? 4 : 6, iname, iface->index, iface->label, recviface->index, recviface->label); continue; } // *** If we reach this point, we know this interface is the one we are looking for ***// // Copy interface name strncpy(next_iface.name, iname, sizeof(next_iface.name)-1); next_iface.name[sizeof(next_iface.name)-1] = '\0'; bool isULA = false, isGUA = false, isLL = false; // Check if this address is different from 0000:0000:0000:0000:0000:0000:0000:0000 if(family == AF_INET6 && memcmp(&next_iface.addr6.addr6, &iface->addr.in6.sin6_addr, sizeof(iface->addr.in6.sin6_addr)) != 0) { // Extract first byte // We do not directly access the underlying union as // MUSL defines it differently than GNU C uint8_t bytes[2]; memcpy(&bytes, &iface->addr.in6.sin6_addr, 2); // Global Unicast Address (2000::/3, RFC 4291) isGUA = (bytes[0] & 0x70) == 0x20; // Unique Local Address (fc00::/7, RFC 4193) isULA = (bytes[0] & 0xfe) == 0xfc; // Link Local Address (fe80::/10, RFC 4291) isLL = (bytes[0] & 0xff) == 0xfe && (bytes[1] & 0x30) == 0; // Store IPv6 address only if we don't already have a GUA or ULA address // This makes the preference: // 1. ULA // 2. GUA // 3. Link-local if((!haveGUAv6 && !haveULAv6) || (haveGUAv6 && isULA)) { next_iface.haveIPv6 = true; // Store IPv6 address memcpy(&next_iface.addr6.addr6, &iface->addr.in6.sin6_addr, sizeof(iface->addr.in6.sin6_addr)); if(isGUA) haveGUAv6 = true; else if(isULA) haveULAv6 = true; } } // Check if this address is different from 0.0.0.0 else if(family == AF_INET && memcmp(&next_iface.addr4.addr4, &iface->addr.in.sin_addr, sizeof(iface->addr.in.sin_addr)) != 0) { next_iface.haveIPv4 = true; // Store IPv4 address memcpy(&next_iface.addr4.addr4, &iface->addr.in.sin_addr, sizeof(iface->addr.in.sin_addr)); } // Debug logging if(config.debug.networking.v.b) { char buffer[ADDRSTRLEN+1] = { 0 }; if(family == AF_INET) inet_ntop(AF_INET, &iface->addr.in.sin_addr, buffer, ADDRSTRLEN); else if(family == AF_INET6) inet_ntop(AF_INET6, &iface->addr.in6.sin6_addr, buffer, ADDRSTRLEN); const char *type = family == AF_INET6 ? isGUA ? " (GUA)" : isULA ? " (ULA)" : isLL ? " (LL)" : " (other)" : ""; log_debug(DEBUG_NETWORKING, " - OK IPv%d interface %s (%d,%d) is %s%s", family == AF_INET ? 4 : 6, next_iface.name, iface->index, iface->label, buffer, type); } // Exit loop early if we already have everything we need // (a valid IPv4 address + a valid ULA IPv6 address) if(next_iface.haveIPv4 && haveULAv6) { log_debug(DEBUG_NETWORKING, "Exiting interface analysis early (have IPv4 + ULAv6)"); break; } } } // 32 nibble labels of an IPv6 address plus "ip6.arpa" #define PTR_NAME_LEN (32*2 + sizeof("ip6.arpa")) // Addresses we have already generated a PTR record for, keyed on the decoded // address rather than the query name (many names decode to one address). An // entry is only added for an address of one of our interfaces, so the table // holds at most the addresses this host has had struct generated_ptr_entry { sa_family_t family; union all_addr addr; }; static struct generated_ptr_entry *generated_ptr = NULL; static unsigned int generated_ptrs = 0; static unsigned int generated_ptrs_size = 0; static bool __attribute__((pure)) ptr_generated_for(const int flags, const union all_addr *addr) { for(unsigned int i = 0; i < generated_ptrs; i++) { if(flags == F_IPV4 && generated_ptr[i].family == AF_INET && generated_ptr[i].addr.addr4.s_addr == addr->addr4.s_addr) return true; if(flags == F_IPV6 && generated_ptr[i].family == AF_INET6 && IN6_ARE_ADDR_EQUAL(&generated_ptr[i].addr.addr6, &addr->addr6)) return true; } return false; } // Write the canonical reverse name of addr, the name dnsmasq matches PTR // queries against, e.g. 5.1.168.192.in-addr.arpa static void ptr_canonical_name(const int flags, const union all_addr *addr, char name[PTR_NAME_LEN]) { if(flags == F_IPV4) { const unsigned char *b = (const unsigned char *)&addr->addr4.s_addr; snprintf(name, PTR_NAME_LEN, "%u.%u.%u.%u.in-addr.arpa", b[3], b[2], b[1], b[0]); return; } // IPv6: one label per nibble, lowest first char *p = name; for(int i = 15; i >= 0; i--) { const unsigned char b = addr->addr6.s6_addr[i]; *p++ = "0123456789abcdef"[b & 0x0f]; *p++ = '.'; *p++ = "0123456789abcdef"[b >> 4]; *p++ = '.'; } strcpy(p, "ip6.arpa"); } static void check_pihole_PTR(char *domain) { // Convert PTR request into numeric form union all_addr addr = {}; const int flags = in_arpa_name_2_addr(domain, &addr); // Check if this is a valid in-addr.arpa (IPv4) or ip6.[int|arpa] (IPv6) // specifier. If not, nothing is to be done here and we return early if(flags == 0) return; // The record is made for the canonical name, whatever spelling the client // used. dnsmasq matches PTR records by name, so a record under another // spelling would never answer the canonical query char name[PTR_NAME_LEN]; ptr_canonical_name(flags, &addr, name); // One record per address, however the client spelled it if(ptr_generated_for(flags, &addr)) { log_debug(DEBUG_QUERIES, "PTR record for the address behind %s exists", domain); return; } // Iterate through the already configured PTR entries in dnsmasq's // structure and check if we already have a PTR record for this name. // This avoids overwriting PTR records manually added by users using // custom dnsmasq config lines like "ptr-record=," for(struct ptr_record *ptr = daemon->ptr; ptr; ptr = ptr->next) { log_debug(DEBUG_EXTRA, "Known PTR record %p: %s -> %s (next = %p)", ptr, ptr->name, ptr->ptr, ptr->next); // DNS names are case-insensitive (RFC 4343), so compare case- // insensitively if(ptr->name != NULL && strcasecmp(ptr->name, name) == 0) { // We already have a PTR record for this name log_debug(DEBUG_QUERIES, "PTR record for %s exists", name); return; } } // We do not want to reply with "pi.hole" to loopback PTRs if((flags == F_IPV4 && addr.addr4.s_addr == htonl(INADDR_LOOPBACK)) || (flags == F_IPV6 && IN6_IS_ADDR_LOOPBACK(&addr.addr6))) return; // If we reached this point, addr contains the address the client requested // a name for. We compare this address against all addresses of the local // interfaces to see if we should reply with "pi.hole" for (struct irec *iface = daemon->interfaces; iface != NULL; iface = iface->next) { const sa_family_t family = iface->addr.sa.sa_family; // If the family matches but the address doesn't, we skip this address if(!(family == AF_INET && flags == F_IPV4 && iface->addr.in.sin_addr.s_addr == addr.addr4.s_addr) && !(family == AF_INET6 && flags == F_IPV6 && IN6_ARE_ADDR_EQUAL(&iface->addr.in6.sin6_addr, &addr.addr6))) continue; // If we reached this point, we have a match between the address the client // asked about and one of our own. Make room to remember the address so // the next spelling of it does not generate a second record if(generated_ptrs == generated_ptrs_size) { const unsigned int size = generated_ptrs_size ? 2 * generated_ptrs_size : 8; struct generated_ptr_entry *grown = realloc(generated_ptr, size * sizeof(*grown)); if(grown == NULL) { log_err("Cannot allocate PTR record for %s: %s", name, strerror(errno)); return; } generated_ptr = grown; generated_ptrs_size = size; } struct ptr_record *pihole_ptr = calloc(1, sizeof(struct ptr_record)); if(pihole_ptr == NULL) { log_err("Cannot allocate PTR record for %s: %s", name, strerror(errno)); return; } // dnsmasq compares this name without a NULL check pihole_ptr->name = strdup(name); if(pihole_ptr->name == NULL) { log_err("Cannot allocate PTR record for %s: %s", name, strerror(errno)); free(pihole_ptr); return; } if(family == AF_INET) { // IPv4 supports conditional domains pihole_ptr->ptr = get_ptrname(&iface->addr.in.sin_addr); } else { // IPv6 does not support conditional domains pihole_ptr->ptr = get_ptrname(NULL); } // If we have a PTR record, we add it to the list if(daemon->ptr != NULL) { // Iterate to the last PTR entry in dnsmasq's structure struct ptr_record *ptr; for(ptr = daemon->ptr; ptr && ptr->next; ptr = ptr->next); // Add our record after the last existing ptr-record ptr->next = pihole_ptr; } else { // We do not have any PTR records yet, so we add our // record as the first one daemon->ptr = pihole_ptr; } generated_ptr[generated_ptrs].family = family; generated_ptr[generated_ptrs].addr = addr; generated_ptrs++; // Debug logging log_debug(DEBUG_QUERIES, "Generating PTR record (%p): %s -> %s", pihole_ptr, pihole_ptr->name, pihole_ptr->ptr); return; } } static bool check_domain_blocked(const char *domain, clientsData *client, queriesData *query, DNSCacheData *dns_cache, enum query_status *new_status, bool *db_okay) { // Return early if this domain is explicitly allowed if(query->flags.allowed) return false; // Check domains against exact blacklist PERF_START(_pcdb_exact); const enum db_result blacklist = in_denylist(domain, dns_cache, client); PERF_END(_pcdb_exact, PERF_STAT_CDB_EXACT); if(blacklist == FOUND) { // Set new status *new_status = QUERY_DENYLIST; blockingreason = "exactly denied"; // We block this domain return true; } // ABP patterns are generated lazily inside in_gravity() only when // exact-match fails and ABP format is enabled. Using a stack struct // avoids all heap allocations in this path. struct abp_patterns abp = { .count = 0, .generated = false }; // Check domain against antigravity. Timed into the same slot as the // gravity call below so the rollup reflects total gravity-DB work // per query regardless of whether the antigravity path hit or missed. int list_id = -1; PERF_START(_pcdb_antigrav); const enum db_result antigravity = in_gravity(domain, &abp, client, true, &list_id); PERF_END(_pcdb_antigrav, PERF_STAT_CDB_GRAVITY); if(antigravity == FOUND) { log_debug(DEBUG_QUERIES, "Allowing query due to antigravity match (list ID %i)", list_id); // Store ID of the matching antigravity list // positive values (incl. 0) are used for domainlists // -1 means "not set" // -2 is gravity list 0 // -3 is gravity list 1 // ... dns_cache->list_id = -1 * (list_id + 2); // Mark query as allowed to prevent further checks such as CNAME // inspection. This ensures antigravity matches have similar effects // than explicitly allowed domains. query->flags.allowed = true; return false; } // Check domains against gravity domains PERF_START(_pcdb_grav); const enum db_result gravity = in_gravity(domain, &abp, client, false, &list_id); PERF_END(_pcdb_grav, PERF_STAT_CDB_GRAVITY); if(gravity == FOUND) { // Set new status *new_status = QUERY_GRAVITY; blockingreason = "gravity blocked"; log_debug(DEBUG_QUERIES, "Blocking query due to gravity match (list ID %i)", list_id); // Store ID of the matching gravity list // see remarks above for the list_id values dns_cache->list_id = -1 * (list_id + 2); // We block this domain return true; } // Check if one of the database lookups returned that the database is // currently busy if(blacklist == LIST_NOT_AVAILABLE || antigravity == LIST_NOT_AVAILABLE || gravity == LIST_NOT_AVAILABLE) { *db_okay = false; // Handle reply to this query as configured if(config.dns.replyWhenBusy.v.busy_reply == BUSY_ALLOW) { log_debug(DEBUG_QUERIES, "Allowing query as gravity database is not available"); // Permit this query // As we set db_okay to false, this allowing here does not enter the // DNS cache so this domain will be rechecked on the next query return false; } else if(config.dns.replyWhenBusy.v.busy_reply == BUSY_REFUSE) { blockingreason = "to be refused (gravity database is not available)"; force_next_DNS_reply = REPLY_REFUSED; *new_status = QUERY_DBBUSY; } else if(config.dns.replyWhenBusy.v.busy_reply == BUSY_DROP) { blockingreason = "to be dropped (gravity database is not available)"; force_next_DNS_reply = REPLY_NONE; *new_status = QUERY_DBBUSY; } else { blockingreason = "to be blocked (gravity database is not available)"; *new_status = QUERY_DBBUSY; } // We block this query return true; } // Check domain against blacklist regex filters // Skipped when the domain is whitelisted or blocked by exact blacklist or gravity PERF_START(_pcdb_regex); const bool regex_deny_hit = in_regex(domain, dns_cache, client->id, REGEX_DENY); PERF_END(_pcdb_regex, PERF_STAT_CDB_REGEX); if(regex_deny_hit) { // Set new status *new_status = QUERY_REGEX; blockingreason = "regex denied"; // Regex may be overwriting reply type for this domain if(dns_cache->force_reply != REPLY_UNKNOWN) force_next_DNS_reply = dns_cache->force_reply; // Retrieve CNAME target from the shared string pool (O(1)). // cname_strpos == 0 means no CNAME target is set. cname_target = (dns_cache->cname_strpos > 0) ? getstr(dns_cache->cname_strpos) : NULL; // Store ID of this regex (fork-private) last_regex_idx = dns_cache->list_id; // We block this domain return true; } // Not blocked because not found on any list return false; } // Special domain checking static bool special_domain(const queriesData *query, const char *domain) { // domain is already lowercase at this point, so we can do // case-sensitive comparisons in here // Mozilla canary domain // Network administrators may configure their networks as follows to signal // that their local DNS resolver implemented special features that make the // network unsuitable for DoH: // DNS queries for the A and AAAA records for the domain // “use-application-dns.net” must respond with either: a response code other // than NOERROR, such as NXDOMAIN (non-existent domain) or SERVFAIL; or // respond with NOERROR, but return no A or AAAA records. // https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https if(config.dns.specialDomains.mozillaCanary.v.b && strcmp(domain, "use-application-dns.net") == 0 && (query->type == TYPE_A || query->type == TYPE_AAAA)) { blockingreason = "Mozilla canary domain"; force_next_DNS_reply = REPLY_NXDOMAIN; return true; } // Apple iCloud Private Relay // Some enterprise or school networks might be required to audit all // network traffic by policy, and your network can block access to // Private Relay in these cases. The user will be alerted that they need // to either disable Private Relay for your network or choose another // network. // The fastest and most reliable way to alert users is to return a // negative answer from your network’s DNS resolver, preventing DNS // resolution for the following hostnames used by Private Relay traffic. // Avoid causing DNS resolution timeouts or silently dropping IP packets // sent to the Private Relay server, as this can lead to delays on // client devices. // > mask.icloud.com // > mask-h2.icloud.com // https://developer.apple.com/support/prepare-your-network-for-icloud-private-relay if(config.dns.specialDomains.iCloudPrivateRelay.v.b && (strcmp(domain, "mask.icloud.com") == 0 || strcmp(domain, "mask-h2.icloud.com") == 0)) { blockingreason = "Apple iCloud Private Relay domain"; force_next_DNS_reply = REPLY_NXDOMAIN; return true; } // RFC 9462: Designated Resolver Domain // The domain "_dns.resolver.arpa" is reserved for use by DNS resolvers // that are designated by the network administrator to provide special // services to the network, e.g., DoH, DoQ, or DoT. // // Example (Google's 8.8.8.8): // ;; QUESTION SECTION: // ;_dns.resolver.arpa. IN SVCB // // ;; ANSWER SECTION: // _dns.resolver.arpa. 86400 IN SVCB 1 dns.google. alpn="dot" // _dns.resolver.arpa. 86400 IN SVCB 2 dns.google. alpn="h2,h3" key7="/dns-query{?dns}" // // RFC 9462, Section 4 says: // // If the recursive resolver that receives this query has no Designated // Resolvers, it SHOULD return NODATA for queries to the "resolver.arpa" // zone, to provide a consistent and accurate signal to clients that it // does not have a Designated Resolver. if(config.dns.specialDomains.designatedResolver.v.b) { const size_t len = strlen(domain); const size_t zonelen = sizeof("resolver.arpa") - 1; // >= rather than >, so the zone apex resolver.arpa itself is // answered instead of being forwarded, and a label boundary, // so notresolver.arpa is not mistaken for part of the zone if(len >= zonelen && strcmp(&domain[len - zonelen], "resolver.arpa") == 0 && (len == zonelen || domain[len - zonelen - 1] == '.')) { blockingreason = "Designated Resolver domain"; force_next_DNS_reply = REPLY_NODATA; return true; } } return false; } static bool FTL_check_blocking(const char *domainstr, queriesData *query, clientsData *client, domainsData *domain, DNSCacheData *dns_cache) { // Only check blocking conditions when global blocking is enabled if(get_blockingstatus() == BLOCKING_DISABLED) { return false; } // Callers supply pre-fetched pointers to avoid redundant SHM lookups. if(query == NULL || domain == NULL || client == NULL || dns_cache == NULL) { log_err("Not enough info, skipping query analysis"); return false; } // Resolve the client's groups first: a change resets the cached // decisions read below, and the allow-regex check relies on them even // when the exact allowlist is skipped gravityDB_ensure_client_groups(client); // If this cache record can expire, check if it is still valid and/or if // caching is generally disabled if((dns_cache->expires > 0 && ABS_TO_SHM_TIME((time_t)query->timestamp) > dns_cache->expires) || config.dns.cache.upstreamBlockedTTL.v.ui == 0) { // This cache record is expired or caching is disabled, we have // to re-check if this domain is blocked log_debug(DEBUG_QUERIES, "DNS cache record expired or caching disabled"); dns_cache->blocking_status = QUERY_UNKNOWN; dns_cache->flags.allowed = false; dns_cache->expires = 0; dns_cache->list_id = -1; dns_cache->force_reply = REPLY_UNKNOWN; dns_cache->cname_strpos = 0; } // Check if the cache record we have applies to the current query // If not, ensure we re-check the domain (happens during CNAME inspection) enum query_status blocking_status = QUERY_UNKNOWN; if(query->clientID == client->id && query->domainID == domain->id) blocking_status = dns_cache->blocking_status; // Memorize blocking status DNS cache for the domain/client combination cacheStatus = blocking_status; log_debug(DEBUG_QUERIES, "Set global cache status to %d", cacheStatus); // Count cache hits (known result, skip gravity.db) vs misses (full lookup needed) if(config.debug.performance.v.b) { if(blocking_status != QUERY_UNKNOWN) ftl_cache_hits++; else ftl_cache_misses++; } // Skip the entire chain of tests if we already know the answer for this // particular client. Use the caller's already-lowercase stack copy rather // than re-reading from SHM (avoids one getstr() dereference). switch(blocking_status) { case QUERY_UNKNOWN: // New domain/client combination. // We have to go through all the tests below log_debug(DEBUG_QUERIES, "%s is not known", domainstr); break; case QUERY_DENYLIST: case QUERY_DENYLIST_CNAME: // Known as exactly denied, we return this result early, skipping // all the lengthy tests below blockingreason = blocking_status == QUERY_DENYLIST ? "exactly denied" : "exactly denied (CNAME)"; log_debug(DEBUG_QUERIES, "%s is known as %s", domainstr, blockingreason); // Do not block if the entire query is to be permitted // as something along the CNAME path hit the whitelist if(!query->flags.allowed) { force_next_DNS_reply = dns_cache->force_reply; query_blocked(query, domain, client, blocking_status); if(blocking_status == QUERY_DENYLIST_CNAME) { query->CNAME_domainID = dns_cache->CNAME_domainID; domainsData *cname_domain = getDomain(dns_cache->CNAME_domainID, true); if(cname_domain != NULL) cname_domain->cname_refcount++; } return true; } break; case QUERY_GRAVITY: case QUERY_GRAVITY_CNAME: // Known as gravity blocked, we return this result early, skipping // all the lengthy tests below blockingreason = blocking_status == QUERY_GRAVITY ? "gravity blocked" : "gravity blocked (CNAME)"; log_debug(DEBUG_QUERIES, "%s is known as %s", domainstr, blockingreason); // Do not block if the entire query is to be permitted // as sometving along the CNAME path hit the whitelist if(!query->flags.allowed) { force_next_DNS_reply = dns_cache->force_reply; query_blocked(query, domain, client, blocking_status); if(blocking_status == QUERY_GRAVITY_CNAME) { query->CNAME_domainID = dns_cache->CNAME_domainID; domainsData *cname_domain = getDomain(dns_cache->CNAME_domainID, true); if(cname_domain != NULL) cname_domain->cname_refcount++; } return true; } break; case QUERY_REGEX: case QUERY_REGEX_CNAME: // Known as regex denied, we return this result early, skipping all // the lengthy tests below blockingreason = blocking_status == QUERY_REGEX ? "regex denied" : "regex denied (CNAME)"; log_debug(DEBUG_QUERIES, "%s is known as %s (cache regex ID: %i)", domainstr, blockingreason, dns_cache->list_id); // Do not block if the entire query is to be permitted as something // along the CNAME path hit the whitelist if(!query->flags.allowed) { force_next_DNS_reply = dns_cache->force_reply; // The CNAME target lives in the cache entry too, and // REPLY_CNAME above is worth nothing without it cname_target = (dns_cache->cname_strpos > 0) ? getstr(dns_cache->cname_strpos) : NULL; last_regex_idx = dns_cache->list_id; query_blocked(query, domain, client, blocking_status); if(blocking_status == QUERY_REGEX_CNAME) { query->CNAME_domainID = dns_cache->CNAME_domainID; domainsData *cname_domain = getDomain(dns_cache->CNAME_domainID, true); if(cname_domain != NULL) cname_domain->cname_refcount++; } return true; } break; case QUERY_SPECIAL_DOMAIN: // Known as a special domain, we return this result early, skipping // all the lengthy tests below blockingreason = "special domain"; log_debug(DEBUG_QUERIES, "%s is known as special domain", domainstr); force_next_DNS_reply = dns_cache->force_reply; query_blocked(query, domain, client, QUERY_SPECIAL_DOMAIN); return true; case QUERY_EXTERNAL_BLOCKED_IP: case QUERY_EXTERNAL_BLOCKED_NULL: case QUERY_EXTERNAL_BLOCKED_NXRA: case QUERY_EXTERNAL_BLOCKED_EDE15: { bool shortcircuit = true; switch(blocking_status) { case QUERY_UNKNOWN: case QUERY_GRAVITY: case QUERY_DENYLIST: case QUERY_REGEX: case QUERY_FORWARDED: case QUERY_CACHE: case QUERY_GRAVITY_CNAME: case QUERY_REGEX_CNAME: case QUERY_DENYLIST_CNAME: case QUERY_RETRIED: case QUERY_RETRIED_DNSSEC: case QUERY_IN_PROGRESS: case QUERY_DBBUSY: case QUERY_SPECIAL_DOMAIN: case QUERY_CACHE_STALE: case QUERY_STATUS_MAX: // Cannot happen break; case QUERY_EXTERNAL_BLOCKED_IP: blockingreason = "blocked upstream with known address"; // We do not want to short-circuit this // query as to get the address contained // in the upstream reply being sent // downstream to the client. // Otherwise, Pi-hole's short-circuiting // would reply to the client with the // configured blocking mode (probably // NULL) shortcircuit = false; break; case QUERY_EXTERNAL_BLOCKED_NULL: blockingreason = "blocked upstream with NULL address"; break; case QUERY_EXTERNAL_BLOCKED_EDE15: blockingreason = "blocked upstream with EDE15"; break; case QUERY_EXTERNAL_BLOCKED_NXRA: blockingreason = "blocked upstream with NXRA address"; break; } // Known as upstream blocked, we return this result // early, skipping all the lengthy tests below log_debug(DEBUG_QUERIES, "%s is known as %s (expires in %lus)", domainstr, blockingreason, (unsigned long)(SHM_TO_ABS_TIME(dns_cache->expires) - (time_t)query->timestamp)); force_next_DNS_reply = dns_cache->force_reply; query_blocked(query, domain, client, blocking_status); return shortcircuit; } case QUERY_CACHE: case QUERY_FORWARDED: case QUERY_RETRIED: case QUERY_RETRIED_DNSSEC: case QUERY_IN_PROGRESS: case QUERY_DBBUSY: case QUERY_CACHE_STALE: case QUERY_STATUS_MAX: // Known as not to be blocked, possibly even explicitly // allowed - we return this result early, skipping all // the lengthy tests below log_debug(DEBUG_QUERIES, "%s is known as not to be blocked%s", domainstr, dns_cache->flags.allowed ? " (allowed)" : ""); if(dns_cache->flags.allowed) query->flags.allowed = true; return false; } // Skip all checks and continue if we hit already at least one allowlist in the chain if(query->flags.allowed) { log_debug(DEBUG_QUERIES, "Query is permitted as at least one allowlist entry matched"); return false; } // when we reach this point: the query is not in FTL's cache (for this client) // Check exact whitelist for match const char *blockedDomain = domainstr; PERF_START(_pcb_allow); TIMED_DB_OP_RESULT(query->flags.allowed, in_allowlist(domainstr, dns_cache, client) == FOUND); // If not found: Check regex whitelist for match if(!query->flags.allowed) TIMED_DB_OP_RESULT(query->flags.allowed, in_regex(domainstr, dns_cache, client->id, REGEX_ALLOW)); PERF_END(_pcb_allow, PERF_STAT_CB_ALLOWLIST); // Check if this is a special domain if(!query->flags.allowed && special_domain(query, domainstr)) { // Set DNS cache properties dns_cache->blocking_status = QUERY_SPECIAL_DOMAIN; cacheStatus = dns_cache->blocking_status; dns_cache->force_reply = force_next_DNS_reply; // Adjust counters query_blocked(query, domain, client, QUERY_SPECIAL_DOMAIN); // Debug output log_debug(DEBUG_QUERIES, "Special domain: %s is %s", domainstr, blockingreason); return true; } // Check blacklist (exact + regex) and gravity for queried domain unsigned char new_status = QUERY_UNKNOWN; bool db_okay = true; bool blockDomain; PERF_START(_pcb_deny); TIMED_DB_OP_RESULT(blockDomain, check_domain_blocked(domainstr, client, query, dns_cache, &new_status, &db_okay)); PERF_END(_pcb_deny, PERF_STAT_CB_DENYLIST); // Check blacklist (exact + regex) and gravity for _esni.domain if enabled // (defaulting to true). Timed into the same slot as the primary call above // so the rollup reflects total denylist/gravity work per query, regardless // of whether the _esni fallback ran. if(config.dns.blockESNI.v.b && !query->flags.allowed && !blockDomain && domainstr[0] == '_' && strncmp(domainstr, "_esni.", 6u) == 0 && domainstr[6] != '\0') { PERF_START(_pcb_deny_esni); TIMED_DB_OP_RESULT(blockDomain, check_domain_blocked(domainstr + 6u, client, query, dns_cache, &new_status, &db_okay)); PERF_END(_pcb_deny_esni, PERF_STAT_CB_DENYLIST); // Update DNS cache status cacheStatus = dns_cache->blocking_status; if(blockDomain) { // Truncate "_esni." from queried domain if the parenting domain was // the reason for blocking this query blockedDomain = domainstr + 6u; // Force next DNS reply to be NXDOMAIN for _esni.* queries force_next_DNS_reply = REPLY_NXDOMAIN; // Store this in the DNS cache only if the database is available at // this point if(db_okay) dns_cache->force_reply = REPLY_NXDOMAIN; } } // Common actions regardless what the possible blocking reason is if(blockDomain) { // The answer built for this query derives its EDE from cacheStatus cacheStatus = new_status; // Adjust counters query_blocked(query, domain, client, new_status); // Debug output if(config.debug.queries.v.b) { log_debug(DEBUG_QUERIES, "Blocking %s as %s is %s (domainlist ID: %i)", domainstr, blockedDomain, blockingreason, dns_cache->list_id); if(force_next_DNS_reply != 0) log_debug(DEBUG_QUERIES, "Forcing next reply to %s", get_query_reply_str(force_next_DNS_reply)); } } else if(db_okay) { // Explicitly mark as not blocked to skip the entire gravity/blacklist // chain when the same client asks for the same domain in the future. // Store domain as allowed if this is the case dns_cache->flags.allowed = query->flags.allowed; // Debug output // client is guaranteed to be non-NULL above log_debug(DEBUG_QUERIES, "DNS cache: %s/%s/%s is %s (domainlist ID: %i)", get_query_type_str(query->type, NULL, NULL), getstr(client->ippos), domainstr, query->flags.allowed ? "allowed" : "not blocked", dns_cache->list_id); } return blockDomain; } /** * @brief Updates the cache record for the "pi.hole" domain with the current interface addresses. * * This function searches the DNS cache for entries corresponding to the "pi.hole" domain, * for both IPv4 and IPv6 address families. For each matching cache entry found, it updates * the stored address with the address from the next available network interface. It also * sets flags indicating the presence of IPv4 and/or IPv6 addresses in the interface structure. */ static void update_pihole_cache_record(void) { struct crec *lookup = NULL; while ((lookup = cache_find_by_name(lookup, (char*)"pi.hole", 0, F_IPV4 | F_IPV6))) { // We have a cache entry for "pi.hole", so we can use it log_debug(DEBUG_NETWORKING, "Found cache entry for pi.hole: %p", lookup); if(lookup->flags & F_IPV4) { if(config.dns.reply.host.force4.v.b) memcpy(&lookup->addr.addr4, &config.dns.reply.host.v4.v.in_addr, sizeof(lookup->addr.addr4)); else memcpy(&lookup->addr.addr4, &next_iface.addr4.addr4, sizeof(lookup->addr.addr4)); log_debug(DEBUG_NETWORKING, "Updating IPv4 address in cache"); } if(lookup->flags & F_IPV6) { if(config.dns.reply.host.force6.v.b) memcpy(&lookup->addr.addr6, &config.dns.reply.host.v6.v.in6_addr, sizeof(lookup->addr.addr6)); else memcpy(&lookup->addr.addr6, &next_iface.addr6.addr6, sizeof(lookup->addr.addr6)); log_debug(DEBUG_NETWORKING, "Updating IPv6 address in cache"); } } } bool FTL_CNAME(const char *dst, const char *src, const int id) { const double now = double_time(); log_debug(DEBUG_QUERIES, "FTL_CNAME called with: src = %s, dst = %s, id = %d", src, dst, id); if((src != NULL && strcasecmp(src, "pi.hole") == 0) || (dst != NULL && strcasecmp(dst, "pi.hole") == 0)) { // If "pi.hole" occurs in the CNAME chain we need to make sure // the "pi.hole" cache record is up-to-date with the current // interface addresses for interface-dependent replies log_debug(DEBUG_QUERIES, "Updating pi.hole cache record as it is part of the CNAME chain"); update_pihole_cache_record(); } // Does the user want to skip deep CNAME inspection? if(!config.dns.CNAMEdeepInspect.v.b) { log_debug(DEBUG_QUERIES, "Skipping analysis as CNAME inspection is disabled"); return false; } // Lock shared memory lock_shm(); // Save status and upstreamID in corresponding query identified by dnsmasq's ID // (negative for TCP queries, stored positive by FTL_new_query) const int queryID = findQueryID(id < 0 ? -id : id); if(queryID < 0) { // This may happen e.g. if the original query was a PTR query // or "pi.hole" and we ignored them altogether unlock_shm(); log_debug(DEBUG_QUERIES, "Skipping analysis as parent query is not found"); return false; } // Get query pointer so we can later extract the client requesting this domain for // the per-client blocking evaluation queriesData *query = getQuery(queryID, true); if(query == NULL) { // Nothing to be done here unlock_shm(); log_debug(DEBUG_QUERIES, "Skipping analysis as parent query is not valid"); return false; } // Example to make the terminology used in here clear: // CNAME abc -> 123 // CNAME 123 -> 456 // CNAME 456 -> 789 // parent_domain: abc // child_domains: [123, 456, 789] // parent_domain = Domain at the top of the CNAME path // This is the domain which was queried first in this chain const int parent_domainID = query->domainID; // child_domain = Intermediate domain in CNAME path // This is the domain which was queried later in this chain char child_domain[MAXDOMAINLEN]; strcpy_tolower(child_domain, dst, sizeof(child_domain)); const int child_domainID = findDomainID(child_domain, false); // Set child domains's last query time if(child_domainID >= 0) { domainsData *cdomain = getDomain(child_domainID, true); if(cdomain != NULL) cdomain->lastQuery = now; } // Get client ID from the original query (the entire chain always // belongs to the same client) const int clientID = query->clientID; clientsData *client = getClient(clientID, true); DNSCacheData *dns_cache = getDNSCache(query->cacheID, true); domainsData *child_domain_data = getDomain(child_domainID, true); if(client == NULL || dns_cache == NULL) { unlock_shm(); log_err("No memory available, skipping CNAME blocking analysis"); return false; } // Check per-client blocking for the child domain. Timed into its own // slot (not PERF_STAT_CHECK_BLOCKING) so the 5-minute rollup splits // primary-query work from CNAME-hop work: one user-facing DNS query // with a 5-hop CNAME chain drives 5 separate calls here, each capable // of a full denylist/gravity lookup. PERF_START(_pcbc); const bool block = FTL_check_blocking(child_domain, query, client, child_domain_data, dns_cache); PERF_END(_pcbc, PERF_STAT_CB_CNAME); // If we find during a CNAME inspection that we want to block the entire chain, // the originally queried domain itself was not counted as blocked. We have to // correct this when we are going to short-circuit the entire query if(block) { // Increase blocked count of parent domain domainsData *parent_domain = getDomain(parent_domainID, true); if(parent_domain == NULL) { // Memory error, return unlock_shm(); return false; } parent_domain->blockedcount++; // Store query response as CNAME type query_set_reply(F_CNAME, 0, NULL, query, now); // Store domain that was the reason for blocking the entire chain query->CNAME_domainID = child_domainID; if(child_domain_data != NULL) child_domain_data->cname_refcount++; // Store CNAME domain ID in DNS cache. A cache entry holds at most // one reference and gives it back once, when runGC() recycles it, // so taking one unconditionally here leaks: this path runs again // for an entry whose blocking status a gravity reload has reset, // and the entry is then counted twice against the same domain. // Take a reference only when the entry is not already holding one // for this domain, and hand back the old one when it moves const int parent_cacheID = query->cacheID > -1 ? query->cacheID : findCacheID(parent_domainID, clientID, query->type, false); DNSCacheData *parent_cache = parent_cacheID < 0 ? NULL : getDNSCache(parent_cacheID, true); if(parent_cache != NULL && parent_cache->CNAME_domainID != (unsigned int)child_domainID) { if(parent_cache->CNAME_domainID != (unsigned int)-1) { domainsData *old_cname = getDomain(parent_cache->CNAME_domainID, true); if(old_cname != NULL) old_cname->cname_refcount--; } parent_cache->CNAME_domainID = child_domainID; if(child_domain_data != NULL) child_domain_data->cname_refcount++; } // Change blocking reason into CNAME-caused blocking if(query->status == QUERY_GRAVITY) { query_set_status(query, QUERY_GRAVITY_CNAME); } else if(query->status == QUERY_REGEX) { // Get child DNS cache entries const int child_cacheID = findCacheID(child_domainID, clientID, query->type, false); // Get child's cache pointer const DNSCacheData *child_cache = child_cacheID < 0 ? NULL : getDNSCache(child_cacheID, true); // Propagate ID of responsible regex up from the child to the parent // domain (but only if set) if(parent_cache != NULL && child_cache != NULL && child_cache->list_id != -1) parent_cache->list_id = child_cache->list_id; // Set status query_set_status(query, QUERY_REGEX_CNAME); } else if(query->status == QUERY_DENYLIST) { // Only set status query_set_status(query, QUERY_DENYLIST_CNAME); } // The answer built for this query derives its EDE from cacheStatus cacheStatus = query->status; } // Debug logging for deep CNAME inspection (if enabled) log_debug(DEBUG_QUERIES, "Query %d: CNAME %s ---> %s", id, src, dst); // Mark query for updating in the database query->flags.database.changed = true; // Return result unlock_shm(); return block; } static void FTL_forwarded(const unsigned int flags, const char *name, const union all_addr *addr, unsigned short port, const int id, const char *file, const int line) { // Save that this query got forwarded to an upstream server const double now = double_time(); // Lock shared memory lock_shm(); // Get forward destination IP address and port in_port_t upstreamPort = 53; char dest[ADDRSTRLEN]; // If addr == NULL, we will only duplicate an empty string instead of uninitialized memory dest[0] = '\0'; if(addr != NULL) { if(flags & F_IPV4) { inet_ntop(AF_INET, addr, dest, ADDRSTRLEN); // Reverse-engineer port from underlying sockaddr_in structure const in_port_t *rport = (in_port_t*)((void*)addr - offsetof(struct sockaddr_in, sin_addr) + offsetof(struct sockaddr_in, sin_port)); upstreamPort = ntohs(*rport); if(upstreamPort != port) log_err("Port mismatch for %s: we derived %d, dnsmasq told us %d", dest, upstreamPort, port); } else { inet_ntop(AF_INET6, addr, dest, ADDRSTRLEN); // Reverse-engineer port from underlying sockaddr_in6 structure const in_port_t *rport = (in_port_t*)((void*)addr - offsetof(struct sockaddr_in6, sin6_addr) + offsetof(struct sockaddr_in6, sin6_port)); upstreamPort = ntohs(*rport); if(upstreamPort != port) log_err("Port mismatch for %s: we derived %d, dnsmasq told us %d", dest, upstreamPort, port); } } // Debug logging log_debug(DEBUG_QUERIES, "**** forwarded %s to %s#%u (ID %i, %s:%i)", name, dest, upstreamPort, id, file, line); // Save status and upstreamID in corresponding query identified by dnsmasq's ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was a PTR query or "pi.hole" // as we ignore them altogether unlock_shm(); return; } // Get query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { unlock_shm(); return; } // Check if this query is already marked as complete // This can happen when multiple upstream servers respond to the same // query or when the query has already been replied to from stale cache // data (cache-optimizer) and this is the followup to refresh the cache // record with possibly changed data if(query->flags.complete) { unlock_shm(); return; } // Get ID of upstream destination, create new upstream record // if not found in current data structure const unsigned int upstreamID = findUpstreamID(dest, upstreamPort); // A query is counted against one upstream at a time. If it is forwarded // again before it is complete, the count moves along if(query->flags.upstream_counted) { upstreamsData *old_upstream = getUpstream(query->upstreamID, true); if(old_upstream != NULL) old_upstream->count--; query->flags.upstream_counted = false; } query->upstreamID = upstreamID; upstreamsData *upstream = getUpstream(upstreamID, true); if(upstream != NULL) { upstream->count++; query->flags.upstream_counted = true; upstream->lastQuery = now; } // Proceed only if // - current query has not been marked as replied to so far // (it could be that answers from multiple forward // destinations are coming in for the same query) // - the query was formally known as cached but had to be forwarded // (this is a special case further described below) if(query->flags.complete && query->status != QUERY_CACHE) { unlock_shm(); return; } if(query->status == QUERY_CACHE) { // Detect if we cached the but need to ask the upstream // servers for the actual IPs now, we remove this query from the // counters for cache replied queries as we had to forward a // request for it. Example: // Assume a domain a.com is a CNAME which is cached and has a very // long TTL. It point to another domain server.a.com which has an // A record but this has a much lower TTL. // If you now query a.com and then again after some time, you end // up in a situation where dnsmasq can answer the first level of // the DNS result (the CNAME) from cache, hence the status of this // query is marked as "answered from cache" in FTLDNS. However, for // server.a.com with the much shorter TTL, we still have to forward // something and ask the upstream server for the final IP address. // Correct reply timer if a response time has already been calculated if(query->flags.response_calculated) { // Reset timer to measure how long it takes until an answer arrives // If a response time has already been calculated, we // can go back in time to measure both the initial cache // lookup and the (now starting) time it takes for the // upstream to respond query->response = now - query->response; query->flags.response_calculated = false; } } else { // Normal forwarded query (status is set below) // Hereby, this query is now fully determined query->flags.complete = true; } // Set query status to forwarded only after the // if(query->status == QUERY_CACHE) { ... } // from above as otherwise this check will always // be negative query_set_status(query, QUERY_FORWARDED); // Mark query for updating in the database query->flags.database.changed = true; // Unlock shared memory unlock_shm(); } static unsigned int reload = 0u; void FTL_dnsmasq_reload(void) { // This function is called by the dnsmasq code on receive of SIGHUP // *before* clearing the cache and re-reading the lists if(reload++ > 0) log_info("Flushing cache and re-reading config"); // Gravity database updates // - (Re-)open gravity database connection // - Get number of blocked domains // - check adlist table for inaccessible adlists // - Read and compile regex filters (incl. per-client) // - Flush FTL's DNS cache set_event(RELOAD_GRAVITY); // Re-read pihole.toml (incl. rewriting) on every but the first reload // (which is happening right after the start of dnsmasq) if(reload > 1) reread_config(); // Re-check capabilities: what FTL needs depends on the configuration, // so this has to see the config the reload just installed check_capabilities(); // Report blocking mode log_info("Blocking status is %s", config.dns.blocking.active.v.b ? "enabled" : "disabled"); // Set resolver as ready resolver_ready = true; } static void alladdr_extract_ip(union all_addr *addr, const sa_family_t family, char ip[ADDRSTRLEN+1]) { // Extract IP address inet_ntop(family, addr, ip, ADDRSTRLEN); } static void mysockaddr_extract_ip_port(const union mysockaddr *server, char ip[ADDRSTRLEN+1], in_port_t *port) { // Extract IP address inet_ntop(server->sa.sa_family, server->sa.sa_family == AF_INET ? (void*)&server->in.sin_addr : (void*)&server->in6.sin6_addr, ip, ADDRSTRLEN); // Extract port (only if requested) if(port != NULL) { *port = ntohs(server->sa.sa_family == AF_INET ? server->in.sin_port : server->in6.sin6_port); } } // Compute cache/upstream response time static inline void set_response_time(queriesData *query, const double now) { // Do this only if this is the first time we set a reply if(query->flags.response_calculated) return; // Convert absolute timestamp to relative timestamp query->response = now - query->response; query->flags.response_calculated = true; } // Changes upstream server (only relevant when multiple servers are defined) // If this is an upstream response and the answering upstream is known (may not // be the case for internally generated DNSSEC queries), we have to check if the // first answering upstream server is also the first one we sent the query to. // If not, we need to change the upstream server associated with this query to // get accurate statistics static void update_upstream(queriesData *query, const int id) { // We use query->flags.response_calculated to check if this is the first // response received for this query and check the family of last server // to see if it is available if(query->flags.response_calculated || last_server.sa.sa_family == 0) return; char ip[ADDRSTRLEN+1] = { 0 }; in_port_t port = 0; mysockaddr_extract_ip_port(&last_server, ip, &port); int upstreamID = findUpstreamID(ip, port); if(upstreamID != query->upstreamID) { // Debug output if(config.debug.queries.v.b && query->upstreamID > -1) { const upstreamsData *upstream = getUpstream(query->upstreamID, true); if(upstream) { const char *oldaddr = getstr(upstream->ippos); const in_port_t oldport = upstream->port; log_debug(DEBUG_QUERIES, "Query ID %d: Associated upstream changed (was %s#%d) as %s#%d replied earlier", id, oldaddr, oldport, ip, port); } } // Move the count along with the attribution. FTL_forwarded() // counted this query against the upstream it first picked and // returns early for every further server of the same forward // round, so the server that actually answered was never counted // and whoever decrements later - query_blocked() or the GC - // would take it off the wrong one if(query->flags.upstream_counted) { upstreamsData *old_upstream = getUpstream(query->upstreamID, true); if(old_upstream != NULL) old_upstream->count--; upstreamsData *new_upstream = upstreamID > -1 ? getUpstream(upstreamID, true) : NULL; if(new_upstream != NULL) new_upstream->count++; else query->flags.upstream_counted = false; } // Update upstream server ID query->upstreamID = upstreamID; } } static void FTL_reply(const unsigned int flags, const char *name, const union all_addr *addr, const char *arg, const int id, const char *file, const int line) { // If domain is "pi.hole", we skip this query // We compare case-insensitive here // Hint: name can be NULL, e.g. for NODATA/NXDOMAIN replies if(name != NULL && strcasecmp(name, "pi.hole") == 0) { return; } // Defer double_time() until after the pi.hole early exit above: that // path returns immediately without using 'now', so computing it first // would waste a clock_gettime vDSO call for every pi.hole reply // (web interface, API, health checks). const double now = double_time(); // Lock shared memory lock_shm(); PERF_START(_prp); // Save status in corresponding query identified by dnsmasq's ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was "pi.hole" log_debug(DEBUG_QUERIES, "FTL_reply(): Query %i has not been found", id); unlock_shm(); return; } // Get and check query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { // Nothing to be done here unlock_shm(); return; } // Check if this reply came from our local cache (query->type == TYPE_NONE is non-query but has F_UPSTREAM) bool cached = false; if(!(flags & F_UPSTREAM) || query->type == TYPE_NONE) { cached = true; if((flags & F_HOSTS) || // hostname.list, /etc/hosts and others ((flags & F_NAMEP) && (flags & F_DHCP)) || // DHCP server reply (flags & F_FORWARD) || // cached answer to previously forwarded request (flags & F_REVERSE) || // cached answer to reverse request (PTR) (flags & F_RRNAME)) // cached answer to TXT query { ; // Okay } else log_debug(DEBUG_FLAGS, "***** Unknown cache query"); } // Is this a stale reply? const bool stale = flags & F_STALE; // Possible debugging output if(config.debug.queries.v.b) { // Human-readable answer may be provided by arg // (e.g. for non-cached queries such as SOA) const char *answer = arg; // Determine returned address (if applicable) char dest[ADDRSTRLEN]; dest[0] = '\0'; if(addr && flags & (F_IPV4 | F_IPV6)) { inet_ntop((flags & F_IPV4) ? AF_INET : AF_INET6, addr, dest, ADDRSTRLEN); answer = dest; // Overwrite answer with human-readable IP address } // Extract answer (used e.g. for detecting if a local config is a user-defined // wildcard blocking entry in form "server=/tobeblocked.com/") if(flags & F_CNAME) answer = "(CNAME)"; else if((flags & F_NEG) && (flags & F_NXDOMAIN)) answer = "(NXDOMAIN)"; else if(flags & F_NEG) answer = "(NODATA)"; else if(flags & F_RCODE && addr != NULL) { unsigned int rcode = addr->log.rcode; if(rcode == REFUSED) { // This happens, e.g., in a "nowhere to forward to" situation answer = "REFUSED (nowhere to forward to)"; } else if(rcode == SERVFAIL) { // This happens on upstream destination errors answer = "SERVFAIL"; } } else if(flags & F_NOEXTRA) { if(flags & F_KEYTAG) answer = "DNSKEY"; else answer = arg; // e.g. "reply is no DS" } // Substitute "." if we are querying the root domain (e.g. DNSKEY) const char *dispname = name; if(!name || strlen(name) == 0) dispname = "."; // Swap display name with answer if this is a reverse query // Check for reverse query by looking at the query type not only // the flag as some PTR queries are not flagged (DNS-SD) if(flags & F_REVERSE || query->type == TYPE_PTR) { const char *tmp = dispname; dispname = answer; answer = tmp; } if(cached || last_server.sa.sa_family == 0) { // Log cache or upstream reply from unknown source log_debug(DEBUG_QUERIES, "**** got %s%s reply: %s is %s (ID %i, %s:%i)", stale ? "stale ": "", cached ? "cache" : "upstream", dispname, answer, id, file, line); } else { char ip[ADDRSTRLEN + 1] = { 0 }; in_port_t port = 0; mysockaddr_extract_ip_port(&last_server, ip, &port); // Log server which replied to our request log_debug(DEBUG_QUERIES, "**** got %s%s reply from %s#%d: %s is %s (ID %i, %s:%i)", stale ? "stale ": "", cached ? "cache" : "upstream", ip, port, dispname, answer, id, file, line); } if(flags & F_RCODE && addr != NULL) { // Translate dnsmasq's rcode into something we can use const char *rcodestr = NULL; enum reply_type reply = REPLY_UNKNOWN; get_rcode(addr->log.rcode, &rcodestr, &reply); // Log RCODE if available log_debug(DEBUG_QUERIES, " RCODE: %s (%d)", rcodestr, addr->log.rcode); } } // EDE analysis if(addr && flags & (F_RCODE | F_SECSTAT) && addr->log.ede != EDE_UNSET) { query->ede = addr->log.ede; log_debug(DEBUG_QUERIES, " EDE (1): %s (%d)", edestr(addr->log.ede), addr->log.ede); } ednsData *edns = getEDNS(id); if(edns != NULL && edns->ede != EDE_UNSET) { query->ede = edns->ede; log_debug(DEBUG_QUERIES, " EDE (2): %s (%d)", edestr(edns->ede), edns->ede); } // Update upstream server (if applicable) if(!cached) update_upstream(query, id); // Reset last_server to avoid possibly changing the upstream server // again in the next query memset(&last_server, 0, sizeof(last_server)); // Save response time // Skipped internally if already computed set_response_time(query, now); // We only process the first reply further in here // Check if reply type is still UNKNOWN if(query->reply != REPLY_UNKNOWN) { // Nothing to be done here unlock_shm(); return; } // Determine if this reply is an exact match for the queried domain const int domainID = query->domainID; // Get domain pointer const domainsData *domain = getDomain(domainID, true); if(domain == NULL) { // Memory error, skip reply unlock_shm(); return; } // Determine query status (live or stale data?) const enum query_status qs = stale ? QUERY_CACHE_STALE : QUERY_CACHE; // This is either a reply served from cache or a blocked query (which appear // to be from cache because of flags containing F_HOSTS) if(cached) { // Set status of this query only if this is not a blocked query if(!is_blocked(query->status)) query_set_status(query, qs); // Save reply type and update individual reply counters query_set_reply(flags, 0, addr, query, now); // We know from cache that this domain is either SECURE or // INSECURE, bogus queries are not cached if(flags & F_DNSSECOK) query_set_dnssec(query, DNSSEC_SECURE); else query_set_dnssec(query, DNSSEC_INSECURE); // Hereby, this query is now fully determined query->flags.complete = true; // Mark query for updating in the database query->flags.database.changed = true; unlock_shm(); return; } // else: This is a reply from upstream // Check if this domain matches exactly const bool isExactMatch = name != NULL && strcasecmp(name, getstr(domain->domainpos)) == 0; if((flags & F_CONFIG) && isExactMatch && !query->flags.complete) { // Answered from local configuration, might be a wildcard or user-provided // Answered from a custom (user provided) cache file or because // we're the authoritative DNS server (e.g. DHCP server and this // is our own domain) query_set_status(query, qs); // Save reply type and update individual reply counters query_set_reply(flags, 0, addr, query, now); // Hereby, this query is now fully determined query->flags.complete = true; // Mark query for updating in the database query->flags.database.changed = true; } else if((flags & (F_FORWARD | F_UPSTREAM)) && isExactMatch && query->type != TYPE_NONE) { // type != 0: Answered from upstream server // type == 0: Answered from cache (probably a non-query reply) if(query->upstreamID < 0) { // This should not happen, but if it does, we skip this // reply log_err("Upstream ID is negative for query %d", id); unlock_shm(); return; } upstreamsData *upstream = getUpstream(query->upstreamID, true); if(upstream == NULL) { // Warning has already been logged by getUpstream(), // skip this reply unlock_shm(); return; } upstream->responses++; // Re-compute upstream average response time and uncertainty upstream->rtime += query->response; const double mean = upstream->rtime / upstream->responses; upstream->rtuncertainty += (mean - query->response)*(mean - query->response); // Only proceed if query is not already known to have been // blocked upstream AND short-circuited. // Note: The reply needs to be analyzed further in case of // QUERY_EXTERNAL_BLOCKED_IP as this is a "normal" upstream // reply and we need to process it further (DNSSEC status, etc.) if(query->status == QUERY_EXTERNAL_BLOCKED_NULL || query->status == QUERY_EXTERNAL_BLOCKED_NXRA || query->status == QUERY_EXTERNAL_BLOCKED_EDE15) { unlock_shm(); return; } // DNSSEC query handling unsigned int reply_flags = flags; if(flags & F_NOEXTRA && (query->type == TYPE_DNSKEY || query->type == TYPE_DS)) { if(flags & F_KEYTAG) { // We were able to validate this query, mark it // as SECURE (reply is {DNSKEY,DS} // keytag , algo , digest ) query_set_dnssec(query, DNSSEC_SECURE); } else if(strstr(arg, "BOGUS") != NULL) { // BOGUS DS query_set_dnssec(query, DNSSEC_BOGUS); } else { // If is a negative reply to a DNSSEC query // (reply is no DS), we overwrite flags // to store NODATA for this query reply_flags = F_NEG; } } // Save reply type and update individual reply counters query_set_reply(reply_flags, 0, addr, query, now); // Mark query for updating in the database query->flags.database.changed = true; } else if(flags & F_REVERSE || query->type == TYPE_PTR) { // isExactMatch is not used here as the PTR is special. // Example: // Question: PTR -x 8.8.8.8 // will lead to: // domain->domain = 8.8.8.8.in-addr.arpa // name = 8.8.8.8 (derived above from addr) // answer = dns.google // Hence, isExactMatch is always false // DNS-SD example: // Question: PTR _http._tcp.local // will lead to: // domain->domain = obs.cr // name = (null) // answer = obs.cr // if flags does not contain F_REVERSE, it is not a reverse // query, e.g. DNS-SD unsigned int pflags = flags; if(!(flags & F_REVERSE)) pflags |= F_RRNAME; // Save reply type and update individual reply counters query_set_reply(pflags, 0, addr, query, now); // Hereby, this query is now fully determined query->flags.complete = true; // Mark query for updating in the database query->flags.database.changed = true; } else if(flags & F_UPSTREAM && flags & F_RCODE) { // Non-query reply synthesized locally query_set_reply(flags, 0, addr, query, now); // Set status of this query if(!is_blocked(query->status)) query_set_status(query, QUERY_CACHE); // Hereby, this query is now fully determined query->flags.complete = true; // Mark query for updating in the database query->flags.database.changed = true; } else if(isExactMatch && !query->flags.complete) { log_warn("Unknown REPLY"); } else if(config.debug.flags.v.b) { log_warn("Unknown upstream REPLY, exact: %s, type: %u", isExactMatch ? "true" : "false", query->type); } if(query && option_bool(OPT_DNSSEC_PROXY)) { // DNSSEC proxy mode is enabled. Interpret AD flag // and set DNSSEC status accordingly query_set_dnssec(query, adbit ? DNSSEC_SECURE : DNSSEC_INSECURE); } PERF_END(_prp, PERF_STAT_REPLY); unlock_shm(); } static enum query_status detect_blocked_IP(const unsigned short flags, const union all_addr *addr) { // Compare returned IP against list of known blocking splash pages // First, we check if we want to skip this result even before comparing against the known IPs if(flags & F_HOSTS || flags & F_REVERSE) { // Skip replies which originated locally. Otherwise, we would // count gravity.list blocked queries as externally blocked. // Also: Do not mark responses of PTR requests as externally blocked. const char *cause = (flags & F_HOSTS) ? "origin is HOSTS" : "query is PTR"; log_debug(DEBUG_QUERIES, "Skipping detection of external blocking IP as %s", cause); // Return early, do not compare against known blocking page IP addresses below return QUERY_UNKNOWN; } // If received one of the following IPs as reply, OpenDNS // (Cisco Umbrella) blocked this query // See https://support.opendns.com/hc/en-us/articles/227986927-What-are-the-Cisco-Umbrella-Block-Page-IP-Addresses // for a full list of these IP addresses const in_addr_t ipv4Addr = (flags & F_IPV4) ? ntohl(addr->addr4.s_addr) : 0; const in_addr_t ipv6Addr = (flags & F_IPV6) ? ntohl(addr->addr6.s6_addr32[3]) : 0; // Check for IP block 146.112.61.104 - 146.112.61.110 if((flags & F_IPV4) && ipv4Addr >= 0x92703d68 && ipv4Addr <= 0x92703d6e) { blockingreason = "blocked upstream with known address (IPv4)"; cacheStatus = QUERY_EXTERNAL_BLOCKED_IP; if(config.debug.queries.v.b) { char answer[ADDRSTRLEN]; answer[0] = '\0'; inet_ntop(AF_INET, addr, answer, ADDRSTRLEN); log_debug(DEBUG_QUERIES, "%s -> \"%s\"", blockingreason, answer); } // Update status return QUERY_EXTERNAL_BLOCKED_IP; } // Check for IP block ::ffff:146.112.61.104 - ::ffff:146.112.61.110 else if(flags & F_IPV6 && addr->addr6.s6_addr32[0] == 0 && addr->addr6.s6_addr32[1] == 0 && addr->addr6.s6_addr32[2] == 0xffff0000 && ipv6Addr >= 0x92703d68 && ipv6Addr <= 0x92703d6e) { blockingreason = "blocked upstream with known address (IPv6)"; cacheStatus = QUERY_EXTERNAL_BLOCKED_IP; if(config.debug.queries.v.b) { char answer[ADDRSTRLEN]; answer[0] = '\0'; inet_ntop(AF_INET6, addr, answer, ADDRSTRLEN); log_debug(DEBUG_QUERIES, "%s -> \"%s\"", blockingreason, answer); } // Update status return QUERY_EXTERNAL_BLOCKED_IP; } // If upstream replied with 0.0.0.0 or ::, // we assume that it filtered the reply as // nothing is reachable under these addresses else if(flags & F_IPV4 && ipv4Addr == 0) { blockingreason = "blocked upstream with 0.0.0.0"; cacheStatus = QUERY_EXTERNAL_BLOCKED_NULL; if(config.debug.queries.v.b) log_debug(DEBUG_QUERIES, "%s", blockingreason); // Update status return QUERY_EXTERNAL_BLOCKED_NULL; } else if(flags & F_IPV6 && addr->addr6.s6_addr32[0] == 0 && addr->addr6.s6_addr32[1] == 0 && addr->addr6.s6_addr32[2] == 0 && addr->addr6.s6_addr32[3] == 0) { blockingreason = "blocked upstream with ::"; cacheStatus = QUERY_EXTERNAL_BLOCKED_NULL; if(config.debug.queries.v.b) log_debug(DEBUG_QUERIES, "%s", blockingreason); // Update status return QUERY_EXTERNAL_BLOCKED_NULL; } // Nothing happened here return QUERY_UNKNOWN; } static void query_blocked(queriesData *query, domainsData *domain, clientsData *client, const enum query_status new_status) { // Adjust counters if we recorded a non-blocking status. Clear the flag // with it: the query keeps pointing at the upstream for the reply and // for the database, but the count has been handed back and the GC must // not hand it back a second time when the query expires if(query->status == QUERY_FORWARDED && query->flags.upstream_counted) { // Get forward pointer upstreamsData *upstream = getUpstream(query->upstreamID, true); if(upstream != NULL) upstream->count--; query->flags.upstream_counted = false; } else if(is_blocked(query->status)) { // Already a blocked query, no need to change anything return; } if(is_blocked(new_status)) { // Count as blocked query. Only the queried domain carries the // count: runGC() hands it back from query->domainID, and a // CNAME hop's domain (FTL_CNAME()) would never get it back if(domain != NULL && domain->id == query->domainID) domain->blockedcount++; if(client != NULL) change_clientcount(client, 0, 1, -1, 0); query->flags.blocked = true; } // Update status query_set_status(query, new_status); // Mark query for updating in the database query->flags.database.changed = true; } static void FTL_dnssec(const char *arg, const union all_addr *addr, const int id, const char *file, const int line) { // Process DNSSEC result for a domain const double now = double_time(); // Lock shared memory lock_shm(); // Search for corresponding query identified by ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was an unhandled query type unlock_shm(); return; } // Get query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { // Memory error, skip this DNSSEC details unlock_shm(); return; } // Debug logging if(config.debug.queries.v.b) { // Get domain pointer const domainsData *domain = getDomain(query->domainID, true); if(domain != NULL) log_debug(DEBUG_QUERIES, "**** DNSSEC %s is %s (ID %i, %s:%i)", getstr(domain->domainpos), arg, id, file, line); if(addr && addr->log.ede != EDE_UNSET) // This function is only called if (flags & F_SECSTAT) log_debug(DEBUG_QUERIES, " EDE: %s (%d)", edestr(addr->log.ede), addr->log.ede); } // Store EDE if(addr && addr->log.ede != EDE_UNSET) query->ede = addr->log.ede; // Iterate through possible values if(strcmp(arg, "SECURE") == 0) query_set_dnssec(query, DNSSEC_SECURE); else if(strcmp(arg, "INSECURE") == 0) query_set_dnssec(query, DNSSEC_INSECURE); else if(strcmp(arg, "BOGUS") == 0) query_set_dnssec(query, DNSSEC_BOGUS); else if(strcmp(arg, "ABANDONED") == 0) query_set_dnssec(query, DNSSEC_ABANDONED); else if(strcmp(arg, "TRUNCATED") == 0) query_set_dnssec(query, DNSSEC_TRUNCATED); else log_warn("Unknown DNSSEC status \"%s\"", arg); // Set reply to NONE (if not already set) as we will not reply to this // query when the status is neither SECURE nor INSECURE if (query->reply == REPLY_UNKNOWN && query->dnssec != DNSSEC_SECURE && query->dnssec != DNSSEC_INSECURE) { query_set_reply(0, REPLY_NONE, addr, query, now); } // Mark query for updating in the database query->flags.database.changed = true; // Unlock shared memory unlock_shm(); } static void get_rcode(const unsigned short rcode, const char **rcodestr, enum reply_type *reply) { // Translate dnsmasq's rcode into something we can use switch(rcode) { case SERVFAIL: *rcodestr = "SERVFAIL"; *reply = REPLY_SERVFAIL; break; case REFUSED: *rcodestr = "REFUSED"; *reply = REPLY_REFUSED; break; case NOTIMP: *rcodestr = "NOT IMPLEMENTED"; *reply = REPLY_NOTIMP; break; default: *rcodestr = "UNKNOWN"; *reply = REPLY_OTHER; break; } } static void FTL_upstream_error(const union all_addr *addr, const unsigned int flags, const int id, const char *file, const int line) { // Process local and upstream errors // Queries with error are those where the RCODE // in the DNS header is neither NOERROR nor NXDOMAIN. // Return early if there is nothing we can analyze here (shouldn't happen) if(!addr) return; // Record response time before queuing for the lock const double now = double_time(); // Lock shared memory lock_shm(); // Search for corresponding query identified by ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was an unhandled query type unlock_shm(); return; } // Get query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { // Memory error, skip this query unlock_shm(); return; } // Update upstream server if necessary update_upstream(query, id); // Translate dnsmasq's rcode into something we can use const char *rcodestr = NULL; enum reply_type reply = REPLY_UNKNOWN; get_rcode(addr->log.rcode, &rcodestr, &reply); // Get EDNS data (if available) ednsData *edns = getEDNS(id); if(addr->log.ede != EDE_UNSET) // This function is only called if (flags & F_RCODE) query->ede = addr->log.ede; else if(edns != NULL && edns->ede != EDE_UNSET) query->ede = edns->ede; // Debug logging if(config.debug.queries.v.b) { // Get domain pointer const domainsData *domain = getDomain(query->domainID, true); // Get domain name const char *domainName = domain != NULL ? getstr(domain->domainpos) : ""; if(flags & F_CONFIG) { // Log local error, typically "nowhere to forward to" log_err("**** local error (nowhere to forward to): %s is %s (ID %i, %s:%i)", domainName, rcodestr, id, file, line); } else if(last_server.sa.sa_family == 0) { // Log error reply from unknown source log_debug(DEBUG_QUERIES, "**** got error reply: %s is %s (ID %i, %s:%i)", domainName, rcodestr, id, file, line); } else { char ip[ADDRSTRLEN+1] = { 0 }; in_port_t port = 0; mysockaddr_extract_ip_port(&last_server, ip, &port); // Log server which replied to our request log_debug(DEBUG_QUERIES, "**** got error reply from %s#%d: %s is %s (ID %i, %s:%i)", ip, port, domainName, rcodestr, id, file, line); } if(query->reply == REPLY_OTHER) log_debug(DEBUG_QUERIES, " Unknown rcode = %i", addr->log.rcode); if(addr->log.ede != EDE_UNSET) log_debug(DEBUG_QUERIES, " EDE: %s (1/%d)", edestr(addr->log.ede), addr->log.ede); if(edns != NULL && edns->ede != EDE_UNSET) log_debug(DEBUG_QUERIES, " EDE: %s (2/%d)", edestr(edns->ede), edns->ede); } // Set query reply query_set_reply(0, reply, addr, query, now); // Mark query for updating in the database query->flags.database.changed = true; // Reset last_server memset(&last_server, 0, sizeof(last_server)); // Unlock shared memory unlock_shm(); } static void FTL_blocked_upstream_by_header(const enum query_status new_status, const int id, const char *file, const int line) { // Get response time const double now = double_time(); // Lock shared memory lock_shm(); // Search for corresponding query identified by ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was an unhandled query type unlock_shm(); return; } // Get query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { // Memory error, skip this query unlock_shm(); return; } // Get domain pointer domainsData *domain = getDomain(query->domainID, true); if(domain == NULL) { // Memory error, skip this query unlock_shm(); return; } // Possible debugging information if(config.debug.queries.v.b) { // Get domain name (domain cannot be NULL here) const char *domainstr = getstr(domain->domainpos); log_debug(DEBUG_QUERIES, "**** %s externally blocked by header (ID %i, FTL %i, %s:%i)", domainstr, id, queryID, file, line); } // Set blocking reason blockingreason = new_status == QUERY_EXTERNAL_BLOCKED_NXRA ? "blocked upstream with NXDOMAIN + no RA" : "blocked upstream with EDE15"; cacheStatus = new_status; // Store query as externally blocked clientsData *client = getClient(query->clientID, true); if(client != NULL) query_blocked(query, domain, client, new_status); // Store reply type as replied with NXDOMAIN query_set_reply(F_NEG | F_NXDOMAIN, 0, NULL, query, now); // Mark query for updating in the database query->flags.database.changed = true; // Unlock shared memory unlock_shm(); } static void FTL_blocked_upstream_by_addr(const enum query_status new_status, const int id, const char *file, const int line) { // Lock shared memory lock_shm(); // Save status in corresponding query identified by dnsmasq's ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was "pi.hole" log_debug(DEBUG_QUERIES, "FTL_check_reply(): Query %i has not been found", id); unlock_shm(); return; } // Get query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { // Memory error, skip this query log_debug(DEBUG_QUERIES, "FTL_check_reply(): Memory error (ID %i)", id); unlock_shm(); return; } clientsData *client = getClient(query->clientID, true); domainsData *domain = getDomain(query->domainID, true); if(client != NULL && domain != NULL) query_blocked(query, domain, client, new_status); // Possible debugging information if(config.debug.queries.v.b) { // Get domain name (domain cannot be NULL here) const char *domainName = domain ? getstr(domain->domainpos) : ""; log_debug(DEBUG_QUERIES, "**** %s externally blocked by address (ID %i, FTL %i, %s:%i)", domainName, id, queryID, file, line); } // Mark query for updating in the database query->flags.database.changed = true; // Unlock shared memory unlock_shm(); } int _FTL_check_reply(const unsigned int rcode, const unsigned short flags, const union all_addr *addr, const int raw_id, const char *file, const int line) { // Get EDE data (if available) const ednsData *edns = getEDNS(raw_id); // The query ID is negative if this is a TCP query const int id = raw_id < 0 ? -raw_id : raw_id; // Check if RA and AA bits are unset in DNS header and rcode is NXDOMAIN // If the response code (rcode) is NXDOMAIN, we may be seeing a response from // an externally blocked query. As they are not always accompany a necessary // SOA record, they are not getting added to our cache and, therefore, // FTL_reply() is never getting called from within the cache routines. // Hence, we have to store the necessary information about the NXDOMAIN // reply already here. // Alternatively, we also consider EDE15 as a blocking reason. if(addr == NULL) { // RA and AA bits are not set and rcode is NXDOMAIN if(!rabit && !aabit && rcode == NXDOMAIN) { FTL_blocked_upstream_by_header(QUERY_EXTERNAL_BLOCKED_NXRA, id, file, line); // Query is blocked return 1; } // EDE 15 if(edns != NULL && edns->ede == EDE_BLOCKED) { FTL_blocked_upstream_by_header(QUERY_EXTERNAL_BLOCKED_EDE15, id, file, line); // Query is blocked return 1; } } // Further checks if this is an IP address else { // Detect if returned IP indicates that this query was blocked const enum query_status new_qstatus = detect_blocked_IP(flags, addr); // Update status of this query if detected as external blocking if(new_qstatus != QUERY_UNKNOWN) { FTL_blocked_upstream_by_addr(new_qstatus, id, file, line); // Query is blocked upstream // Return true for any status except known blocking page // IP address to short-circut the answer. In the latter case, // we want to continue processing the query to get the correct // reply downstream to the requesting client. return new_qstatus != QUERY_EXTERNAL_BLOCKED_IP; } } return 0; } void _FTL_header_analysis(const struct dns_header *header, const struct server *server, const int id, const char *file, const int line) { // Analyze DNS header bits // Check if AD bit is set in DNS header adbit = header->hb4 & HB4_AD; // Check if RA and AA bit is set in DNS header. We do it here as it is it is // forced by dnsmasq shortly after calling FTL_header_analysis() rabit = header->hb4 & HB4_RA; aabit = header->hb3 & HB3_AA; // Store server which sent this reply (if applicable) if(server) { memcpy(&last_server, &server->addr, sizeof(last_server)); if(config.debug.extra.v.b) { char ip[ADDRSTRLEN+1] = { 0 }; in_port_t port = 0; mysockaddr_extract_ip_port(&last_server, ip, &port); log_debug(DEBUG_EXTRA, "Got forward address: %s#%u for ID %i (%s:%i)", ip, port, id, short_path(file), line); } } else { memset(&last_server, 0, sizeof(last_server)); log_debug(DEBUG_EXTRA, "Got forward address: NO for ID %i (%s:%i)", id, short_path(file), line); } } void print_flags(const unsigned int flags) { // Debug function, listing resolver flags in clear text // e.g. "Flags: F_FORWARD F_NEG F_IPV6" // Only print flags if corresponding debugging flag is set if(!(config.debug.flags.v.b)) return; char *flagstr = calloc(sizeof(flagnames) + 1, sizeof(char)); for (unsigned int i = 0; i < ArraySize(flagnames); i++) if (flags & (1u << i)) strcat(flagstr, flagnames[i]); log_debug(DEBUG_FLAGS, " Flags: %s", flagstr); free(flagstr); } static void _query_set_reply(const unsigned int flags, const enum reply_type reply, const union all_addr *addr, queriesData *query, const double now, const char *file, const int line) { enum reply_type new_reply = REPLY_UNKNOWN; // If reply is set, we use it directly instead of interpreting the flags if(reply != 0) { new_reply = reply; } // else: Iterate through possible values by analyzing both the flags and the addr bits else if(flags & F_NEG || (flags & F_NOERR && !(flags & (F_IPV4 | F_IPV6))) || // <-- FTL_make_answer() when no A or AAAA is added force_next_DNS_reply == REPLY_NXDOMAIN || force_next_DNS_reply == REPLY_NODATA) { if(flags & F_NXDOMAIN || force_next_DNS_reply == REPLY_NXDOMAIN) // NXDOMAIN new_reply = REPLY_NXDOMAIN; else // NODATA(-IPv6) new_reply = REPLY_NODATA; } else if(flags & F_CNAME) // new_reply = REPLY_CNAME; else if(flags & F_REVERSE) // reserve lookup new_reply = REPLY_DOMAIN; else if(flags & F_RRNAME) // TXT query new_reply = REPLY_RRNAME; else if((flags & F_RCODE && addr != NULL) || force_next_DNS_reply == REPLY_REFUSED) { if((addr != NULL && addr->log.rcode == REFUSED) || force_next_DNS_reply == REPLY_REFUSED ) { // REFUSED query new_reply = REPLY_REFUSED; } else if(addr != NULL && addr->log.rcode == SERVFAIL) { // SERVFAIL query new_reply = REPLY_SERVFAIL; } else if(addr != NULL && addr->log.rcode == NOTIMP) { // NOTIMP query new_reply = REPLY_NOTIMP; } else { // Other RCODE new_reply = REPLY_OTHER; } } else if(flags & F_KEYTAG && flags & F_NOEXTRA) { // Since 451bd35ad62c1444b3ef1d204ab606c0098b2fd9, F_KEYTAG is // overloaded to discriminate cache records between an arbitrary // RR stored entirely in the addr union and one which has a // point to block storage new_reply = REPLY_DNSSEC; } else if(force_next_DNS_reply == REPLY_NONE) { new_reply = REPLY_NONE; } else if(flags & (F_IPV4 | F_IPV6)) { // IP address new_reply = REPLY_IP; } else { // Other binary, possibly proprietry, data new_reply = REPLY_BLOB; } if(config.debug.queries.v.b) { const char *path = short_path(file); log_debug(DEBUG_QUERIES, "Set reply to %s (%d) in %s:%d", get_query_reply_str(new_reply), new_reply, path, line); if(query->reply != REPLY_UNKNOWN && query->reply != new_reply) log_debug(DEBUG_QUERIES, "Reply of query %i was %s now changing to %s", query->id, get_query_reply_str(query->reply), get_query_reply_str(new_reply)); } // Subtract from old reply counter counters->reply[query->reply]--; log_debug(DEBUG_STATUS, "reply type %u removed (set_reply), ID = %d, new count = %u", query->reply, query->id, counters->reply[query->reply]); // Add to new reply counter counters->reply[new_reply]++; // Store reply type query->reply = new_reply; log_debug(DEBUG_STATUS, "reply type %u added (set_reply), ID = %d, new count = %u", query->reply, query->id, counters->reply[query->reply]); // Save response time // Skipped internally if already computed set_response_time(query, now); } // Hand a file to the pihole user only when it sits directly in /etc/pihole or // /var/log/pihole. The path comes from the pihole-owned config while we still // run as root, and the pihole user cannot swap out either directory static void chown_pihole_own_dir(const char *path, struct passwd *pwd) { if(in_pihole_dir(path)) chown_pihole(path, pwd); else log_warn("Not changing ownership of %s, it is outside of /etc/pihole and /var/log/pihole", path); } void FTL_fork_and_bind_sockets(struct passwd *ent_pw, bool dnsmasq_start) { // Going into daemon mode involves storing the // PID of the generated child process. If FTL // is asked to stay in foreground, we just save // the PID of the current process in the PID file if(daemonmode) go_daemon(); // Initialize query database (pihole-FTL.db) db_init(); // Initialize in-memory databases if(!init_memory_database()) log_crit("Cannot initialize in-memory database."); // Verify checksum of this binary early on to ensure that the binary is // not corrupted and that the binary is not tampered with. We can only // do this here as we need the database to be properly initialized // in case we need to store the verification result verify_FTL(false); // Handle real-time signals in this process (and its children) // Helper processes are already split from the main instance // so they will not listen to real-time signals handle_realtime_signals(); // Initialize thread attributes object with default attribute values // Do NOT detach threads as we want to join them during shutdown with a // fixed timeout to give them time to clean up and finish their work pthread_attr_t attr; pthread_attr_init(&attr); // Take CAP_CHOWN out of use before the worker threads below are created. // Capability sets are per-thread and a new thread inherits a copy of its // creator's, so every thread starts without it in its effective, // inheritable and ambient sets and nothing FTL executes can receive it. // The permitted copy stays: the RTC code raises the capability for the // moment it needs it, and main() hands it to a restarted FTL if(getuid() != 0 && suspend_capability(CAP_CHOWN)) log_debug(DEBUG_CAPS, "Suspended CAP_CHOWN"); // Start NTP sync thread ntp_start_sync_thread(&attr); // Restore sessions from database before starting the database thread to // ensure that sessions import is not blocked by asynchronous query // import in the database thread init_api_sessions(); // Start database thread if database is used if(pthread_create( &threads[DB], &attr, DB_thread, NULL ) != 0) { log_crit("Unable to create database thread. Exiting..."); exit(EXIT_FAILURE); } // Start thread that will stay in the background until garbage // collection needs to be done if(pthread_create( &threads[GC], &attr, GC_thread, NULL ) != 0) { log_crit("Unable to create GC thread. Exiting..."); exit(EXIT_FAILURE); } // Start thread that will stay in the background until host names needs to // be resolved. If configuration does not ask for never resolving hostnames // (e.g. on CI builds), the thread is never started) if(dnsmasq_start && resolve_names() && pthread_create( &threads[DNSclient], &attr, DNSclient_thread, NULL ) != 0) { log_crit("Unable to create DNS client thread. Exiting..."); exit(EXIT_FAILURE); } // Start thread that checks various timers, e.g., for automatic changing // blocking mode (enabled/disabled for a given amount of time) if(pthread_create( &threads[TIMER], &attr, timer, NULL ) != 0) { log_crit("Unable to create timer thread. Exiting..."); exit(EXIT_FAILURE); } #ifdef HAVE_MBEDTLS // Start webserver thread if(pthread_create( &threads[WEBSERVER], &attr, webserver_thread, NULL ) != 0) { log_crit("Unable to create webserver thread. Exiting..."); exit(EXIT_FAILURE); } #else // Initialize FTL HTTP server http_init(); #endif /* HAVE_MBEDTLS */ // Chown files if FTL started as user root but a dnsmasq config // option states to run as a different user/group (e.g. "nobody") if(getuid() == 0) { // Only print this and change ownership of shmem objects when // we're actually dropping root (user/group may be set to root) if(ent_pw != NULL && ent_pw->pw_uid != 0) { log_info("FTL is going to drop from root to user pihole"); // Change ownership of shared memory objects chown_all_shmem(ent_pw); // Configured FTL log file chown_pihole_own_dir(config.files.log.ftl.v.s, ent_pw); // Configured FTL database file chown_pihole_own_dir(config.files.database.v.s, ent_pw); // Temporary history database (database.forceDisk) if(config.database.forceDisk.v.b) chown_pihole_own_dir(config.files.tmp_db.v.s, ent_pw); // Check if auxiliary files exist and change ownership char *extrafile = calloc(strlen(config.files.database.v.s) + 5, sizeof(char)); if(extrafile == NULL) { log_err("Memory allocation failed. Skipping some file ownership checks."); return; } // Check -wal file (write-ahead log) strcpy(extrafile, config.files.database.v.s); strcat(extrafile, "-wal"); if(file_exists(extrafile)) chown_pihole_own_dir(extrafile, ent_pw); // Check -shm file (mmapped shared memory) strcpy(extrafile, config.files.database.v.s); strcat(extrafile, "-shm"); if(file_exists(extrafile)) chown_pihole_own_dir(extrafile, ent_pw); // Free allocated memory free(extrafile); } else { log_info("FTL is running as root"); } } else { uid_t uid; struct passwd *current_user; if ((current_user = getpwuid(uid = geteuid())) != NULL) log_info("FTL is running as user %s (UID %d)", current_user->pw_name, (int)current_user->pw_uid); else log_info("Failed to obtain information about FTL user"); } forked = true; } static char *get_ptrname(const struct in_addr *addr) { // Determine the name Pi-hole should reply with to PTR queries for its own // interface addresses. // // PTR_PIHOLE and PTR_HOSTNAME are independent of the queried address and // return a stable string. PTR_HOSTNAMEFQDN, however, appends a domain // suffix that may differ per address when conditional domains // (domain=,
) are configured. We must therefore not // cache a single result across addresses: the caller stores the returned // pointer in a persistent per-interface PTR record, so each address needs // its own string. check_pihole_PTR() ensures we are called at most once // per address, so the per-address allocation is bounded. switch (config.dns.piholePTR.v.ptr_type) { default: case PTR_MAX: case PTR_NONE: case PTR_PIHOLE: return (char*)"pi.hole"; case PTR_HOSTNAME: return (char*)hostname(); case PTR_HOSTNAMEFQDN: { // get_domain() will also check conditional domains configured like // domain=[,
[,local]] const char *suffix = addr ? get_domain(*addr) : daemon->domain_suffix; // If local suffix is not available, we try to obtain the domain from // the kernel similar to how we do it for the hostname if(!suffix) suffix = (char*)domainname(); // If local suffix is still not available, we substitute // "no_fqdn_available", see the comment about PIHOLE_PTR=HOSTNAMEFQDN // in the Pi-hole docs for further details on why this was chosen if(!suffix || suffix[0] == '\0') suffix = (char*)"no_fqdn_available"; // Build a fresh "." string for this address char *ptrname = calloc(strlen(hostname()) + strlen(suffix) + 2, sizeof(char)); if(!ptrname) // Fallback to "" on memory error return (char*)hostname(); strcpy(ptrname, hostname()); strcat(ptrname, "."); strcat(ptrname, suffix); return ptrname; } } } void FTL_forwarding_retried(struct frec *forward, const int newID, const bool dnssec) { // Forwarding to upstream server failed const struct server *serv = forward->sentto; const int oldID = forward->frec_src.log_id; if(oldID == newID) { log_debug(DEBUG_QUERIES, "%d: Ignoring self-retry", oldID); return; } // Lock shared memory lock_shm(); // Try to obtain destination IP address if available char dest[ADDRSTRLEN]; in_port_t upstreamPort = 53; dest[0] = '\0'; if(serv != NULL) { if(serv->addr.sa.sa_family == AF_INET) { inet_ntop(AF_INET, &serv->addr.in.sin_addr, dest, ADDRSTRLEN); upstreamPort = ntohs(serv->addr.in.sin_port); } else { inet_ntop(AF_INET6, &serv->addr.in6.sin6_addr, dest, ADDRSTRLEN); upstreamPort = ntohs(serv->addr.in6.sin6_port); } } // Get upstream ID (dest is already NUL-terminated by inet_ntop) const int upstreamID = findUpstreamID(dest, upstreamPort); // Possible debugging information log_debug(DEBUG_QUERIES, "**** RETRIED%s query %i as %i to %s#%d", dnssec ? " DNSSEC" : "", oldID, newID, dest, upstreamPort); // Get upstream pointer upstreamsData *upstream = getUpstream(upstreamID, true); // Update counter if(upstream != NULL) upstream->failed++; // Search for corresponding query identified by ID // Retried DNSSEC queries are ignored, we have to flag themselves (newID) // Retried normal queries take over, we have to flag the original query (oldID) const int queryID = findQueryID(dnssec ? newID : oldID); if(queryID >= 0) { // Get query pointer queriesData *query = getQuery(queryID, true); // Set retried status if(query != NULL) { if(dnssec) { // There is no point in retrying the query when // we've already got an answer to this query, // but we're awaiting keys for DNSSEC // validation. We're retrying the DNSSEC query // instead query_set_status(query, QUERY_RETRIED_DNSSEC); } else { // Normal query retry due to answer not arriving // soon enough at the requestor query_set_status(query, QUERY_RETRIED); } // Mark query for updating in the database query->flags.database.changed = true; } } // Unlock shared memory unlock_shm(); return; } // Called when a (forked) TCP worker is terminated by receiving SIGALRM // We close the dedicated database connection this client had opened // to avoid dangling database locks volatile atomic_flag worker_already_terminating = ATOMIC_FLAG_INIT; void FTL_TCP_worker_terminating(bool finished) { if(!finished) { // Both callers passing false are inside dnsmasq's sig_handler() // (dnsmasq.c, the SIGALRM arms), and both _exit(0) right after. // Nothing below this point may run there: log_debug() allocates, // lock_shm() takes a process-shared mutex the interrupted code // may be holding mid-update, and gravityDB_close() finalizes // statements that code may still be stepping. None of it is // async-signal-safe, and none of it is needed - the worker's // connections go with the process, and the SHM mutex is robust, // so the parent recovers it through EOWNERDEAD return; } if(get_dnsmasq_debug()) { // Nothing to be done here, forking does not happen in debug mode return; } if(atomic_flag_test_and_set(&worker_already_terminating)) { log_debug(DEBUG_ANY, "TCP worker already terminating!"); return; } // Possible debug logging if(config.debug.queries.v.b) { const char *reason = finished ? "client disconnected" : "timeout"; log_debug(DEBUG_ANY, "TCP worker terminating (%s)", reason); } if(main_pid() == getpid()) { // If this is not really a fork (e.g. in debug mode), we don't // actually close gravity here return; } // First check if we already locked before. This can happen when a fork // is running into a timeout while it is still processing something and // still holding a lock. if(!is_our_lock()) lock_shm(); // Close dedicated database connections of this fork gravityDB_close(); unlock_shm(); } // Called when a (forked) TCP worker is created // FTL forked to handle TCP connections with dedicated (forked) workers // SQLite3's mentions that carrying an open database connection across a // fork() can lead to all kinds of locking problems as SQLite3 was not // intended to work under such circumstances. Doing so may easily lead // to ending up with a corrupted database. void FTL_TCP_worker_created(const int confd) { if(get_dnsmasq_debug()) { // Nothing to be done here, TCP worker forking does not happen // in debug mode return; } // This runs in the freshly forked TCP worker process. Drop the cached // lock-owner PID/TID inherited from the parent before we take any SHM // lock, otherwise is_our_lock() would compare against the parent's IDs. reset_lock_owner_cache(); // Print this if debugging is enabled if(config.debug.queries.v.b) { // Get peer IP address (client) char peer_ip[ADDRSTRLEN] = { 0 }; union mysockaddr peer_sockaddr = {}; socklen_t peer_len = sizeof(union mysockaddr); if (getpeername(confd, (struct sockaddr *)&peer_sockaddr, &peer_len) != -1) { union all_addr peer_addr = {}; if (peer_sockaddr.sa.sa_family == AF_INET6) peer_addr.addr6 = peer_sockaddr.in6.sin6_addr; else peer_addr.addr4 = peer_sockaddr.in.sin_addr; inet_ntop(peer_sockaddr.sa.sa_family, &peer_addr, peer_ip, ADDRSTRLEN); } // Get local IP address (interface) char local_ip[ADDRSTRLEN] = { 0 }; union mysockaddr iface_sockaddr = {}; socklen_t iface_len = sizeof(union mysockaddr); if(getsockname(confd, (struct sockaddr *)&iface_sockaddr, &iface_len) != -1) { union all_addr iface_addr = {}; if (iface_sockaddr.sa.sa_family == AF_INET6) iface_addr.addr6 = iface_sockaddr.in6.sin6_addr; else iface_addr.addr4 = iface_sockaddr.in.sin_addr; inet_ntop(iface_sockaddr.sa.sa_family, &iface_addr, local_ip, ADDRSTRLEN); } // Print log log_debug(DEBUG_ANY, "TCP worker forked for client %s on interface %s with IP %s", peer_ip, next_iface.name, local_ip); } if(main_pid() == getpid()) { // If this is not really a fork (e.g. in debug mode), we don't // actually re-open gravity or close sockets here return; } // Reopen gravity database handle in this fork as the main process's // handle isn't valid here log_debug(DEBUG_ANY, "Reopening Gravity database for this fork"); gravityDB_forked(); } // A DHCP lease deletion requested over the API. dnsmasq's main thread owns the // lease list and prunes it on its own timer, so the deletion has to run there, // not on the webserver worker that received the request. A worker parks one of // these, wakes the main thread through the event pipe and waits for it to fill // in the result. The request lives on the worker's stack for as long as the // worker is blocked here, so the main thread may use it until it sets `done`. struct lease_delete { const char *ipaddr; // in: address to remove const char *hint; // out: reason on failure, NULL when simply absent bool found; // out: whether a lease was removed bool done; // out: set once the main thread is finished with it struct lease_delete *next; }; static pthread_mutex_t lease_delete_lock = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t lease_delete_cond = PTHREAD_COND_INITIALIZER; static struct lease_delete *lease_delete_queue = NULL; // Remove one lease. Runs only on dnsmasq's main thread (from the EVENT_ALARM // handler), so touching the lease list needs no further synchronization. The // caller writes the lease file and refreshes DNS once for the whole batch. static bool unlink_one_lease(const char *ipaddr, const char **hint, const time_t now) { struct dhcp_lease *lease; union all_addr addr; if (inet_pton(AF_INET, ipaddr, &addr.addr4) > 0) lease = lease_find_by_addr(addr.addr4); #ifdef HAVE_DHCP6 else if (inet_pton(AF_INET6, ipaddr, &addr.addr6) > 0) lease = lease6_find_by_plain_addr(&addr.addr6); #endif else { *hint = "invalid target address (neither IPv4 nor IPv6)"; return false; } if (lease == NULL) { *hint = NULL; return false; } lease_prune(lease, now); return true; } // Drain the queue on dnsmasq's main thread and wake the parked workers. Called // from the EVENT_ALARM handler, which writes the lease file and updates DNS // right after, so a single removal here does not do so itself. void FTL_process_lease_deletions(void) { const time_t now = dnsmasq_time(); pthread_mutex_lock(&lease_delete_lock); for(struct lease_delete *req = lease_delete_queue; req != NULL; req = req->next) { req->found = unlink_one_lease(req->ipaddr, &req->hint, now); req->done = true; } lease_delete_queue = NULL; pthread_cond_broadcast(&lease_delete_cond); pthread_mutex_unlock(&lease_delete_lock); } bool FTL_unlink_DHCP_lease(const char *ipaddr, const char **hint) { if(!daemon->dhcp) { *hint = "DHCP is not enabled"; return false; } // Hand the deletion to the main thread and wait for it. The request stays // on this stack while we block on the condition variable below, so the // pointer parked in the queue stays valid until the main thread is done. struct lease_delete req = { .ipaddr = ipaddr }; pthread_mutex_lock(&lease_delete_lock); req.next = lease_delete_queue; lease_delete_queue = &req; // Wake the main thread. EVENT_ALARM already prunes expired leases and // writes the lease file there; FTL_process_lease_deletions() runs in the // same handler, so a batched write covers our removal too. queue_event(EVENT_ALARM); // Bound the wait so a stuck main thread cannot hang the worker forever struct timespec deadline; clock_gettime(CLOCK_REALTIME, &deadline); deadline.tv_sec += 5; int rc = 0; while(!req.done && rc == 0) rc = pthread_cond_timedwait(&lease_delete_cond, &lease_delete_lock, &deadline); if(!req.done) { // Timed out: take the request back out of the queue before it leaves // this stack, so the main thread cannot dereference it afterwards for(struct lease_delete **pp = &lease_delete_queue; *pp != NULL; pp = &(*pp)->next) if(*pp == &req) { *pp = req.next; break; } pthread_mutex_unlock(&lease_delete_lock); *hint = "timed out waiting for the resolver to remove the lease"; return false; } pthread_mutex_unlock(&lease_delete_lock); *hint = req.hint; return req.found; } void FTL_query_in_progress(const int id) { // Query (possibly from new source), but the same query may be in // progress from another source. // Lock shared memory lock_shm(); // Search for corresponding query identified by ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was an unhandled query type unlock_shm(); return; } // Get query pointer queriesData *query = getQuery(queryID, true); if(query == NULL) { // Memory error, skip this DNSSEC details unlock_shm(); return; } // Debug logging if(config.debug.queries.v.b) { // Get domain pointer const domainsData *domain = getDomain(query->domainID, true); if(domain != NULL) { log_debug(DEBUG_QUERIES, "**** query for %s is already in progress (ID %i)", getstr(domain->domainpos), id); } } // Store status query_set_status(query, QUERY_IN_PROGRESS); // Mark query for updating in the database query->flags.database.changed = true; // Unlock shared memory unlock_shm(); } void FTL_multiple_replies(const int id, int *firstID) { // We are in the loop that iterates over all aggregated queries for the same // type + domain. Every query will receive the reply here so we need to // update the original queries to set their status // Don't process self-duplicates if(*firstID == id) return; // Skip if the original query was not found in FTL's memory if(*firstID == -2) return; // Lock shared memory lock_shm(); // Search for corresponding query identified by ID const int queryID = findQueryID(id); if(queryID < 0) { // This may happen e.g. if the original query was an unhandled query type unlock_shm(); *firstID = -2; return; } if(*firstID == -1) { // This is not yet a duplicate, we just store the ID // of the successful reply here so we can get it quicker // during the next loop iterations unlock_shm(); *firstID = queryID; return; } // Get (read-only) pointer of the query that contains all relevant // information (all others are mere duplicates and were only added to the // list of duplicates rather than havong been forwarded on their own) const queriesData *source_query = getQuery(*firstID, true); // Get query pointer of duplicated reply queriesData *duplicated_query = getQuery(queryID, true); if(duplicated_query == NULL || source_query == NULL) { // Memory error, skip this duplicate unlock_shm(); return; } // Debug logging log_debug(DEBUG_QUERIES, "**** sending reply %d also to %d", *firstID, queryID); // Copy relevant information over counters->reply[duplicated_query->reply]--; log_debug(DEBUG_STATUS, "duplicated_query reply type %u removed, ID = %d, new count = %u", duplicated_query->reply, duplicated_query->id, counters->reply[duplicated_query->reply]); duplicated_query->reply = source_query->reply; counters->reply[duplicated_query->reply]++; log_debug(DEBUG_STATUS, "duplicated_query reply type %u set, ID = %d, new count = %u", duplicated_query->reply, duplicated_query->id, counters->reply[duplicated_query->reply]); duplicated_query->dnssec = source_query->dnssec; duplicated_query->flags.complete = true; duplicated_query->CNAME_domainID = source_query->CNAME_domainID; if(duplicated_query->CNAME_domainID > -1) { domainsData *cname_domain = getDomain(duplicated_query->CNAME_domainID, true); if(cname_domain != NULL) cname_domain->cname_refcount++; } // The original query may have been blocked during CNAME inspection, // correct status in this case if(source_query->status != QUERY_FORWARDED) query_set_status(duplicated_query, source_query->status); // Mark query for updating in the database duplicated_query->flags.database.changed = true; // Unlock shared memory unlock_shm(); } static void _query_set_dnssec(queriesData *query, const enum dnssec_status dnssec, const char *file, const int line) { // Return early if DNSSEC validation is disabled if(!option_bool(OPT_DNSSEC_VALID) && !option_bool(OPT_DNSSEC_PROXY)) return; if(config.debug.dnssec.v.b) { const char *path = short_path(file); const char *status = get_query_dnssec_str(dnssec); log_debug(DEBUG_DNSSEC, "Setting DNSSEC status to %s in %s:%d", status, path, line); } // Set DNSSEC status query->dnssec = dnssec; } // Add dnsmasq log line to internal FIFO buffer (can be queried via the API) void FTL_dnsmasq_log(const char *payload, const int length) { // Lock SHM lock_shm(); // Add to FIFO buffer add_to_fifo_buffer(FIFO_DNSMASQ, payload, NULL, length); // Unlock SHM unlock_shm(); } static const char *check_dnsmasq_name(const char *name) { // Special domain name handling if(!name) // 1. Substitute "(NULL)" if no name is available (should not happen) return "(NULL)"; else if(!name[0]) // 2. Substitute "." if we are querying the root domain (e.g. DNSKEY) return "."; // else return name; } void get_dnsmasq_metrics_obj(cJSON *json) { for (unsigned int i = 0; i < __METRIC_MAX; i++) cJSON_AddNumberToObject(json, get_metric_name(i), daemon->metrics[i]); } void FTL_connection_error(const char *reason, const union mysockaddr *addr, const char where) { // Backup errno const int errnum = errno; // Get the error message const char *error = strerror(errnum); // Set log priority int priority = LOG_ERR; // Additional information (if available) const char *extra = ""; if(where == 1) extra = " while connecting to upstream"; else if(where == 2) extra = " while sending data upstream"; else if(where == 3) extra = " while receiving payload length from upstream"; else if(where == 4) extra = " while receiving payload data from upstream"; // If this is a TCP connection error and errno == 0, this isn't a // connection error but the remote side closed the connection if(errnum == 0 && strcmp(reason, "TCP connection failed") == 0) { error = "Connection prematurely closed by remote server"; priority = LOG_INFO; } // Format the address into a string (if available) in_port_t port = 0; char ip[ADDRSTRLEN + 1] = { 0 }; if(addr != NULL) mysockaddr_extract_ip_port(addr, ip, &port); // Get query ID, may be negative if this is a TCP query const int id = daemon->log_display_id > 0 ? daemon->log_display_id : -daemon->log_display_id; // Log to FTL.log log_debug(DEBUG_QUERIES, "Connection error (%s#%u, ID %d): %s (%s)%s", ip, port, id, reason, error, extra); // Log to pihole.log my_syslog(priority, "%s: %s", reason, error); // Add to Pi-hole diagnostics but do not add messages more often than // once every five seconds to avoid hammering the database with errors // on continuously failing connections static time_t last = 0; if(time(NULL) - last > 5) { // Update last time last = time(NULL); // Build server string char *server = NULL; if(ip[0] != '\0') { const size_t len = strlen(ip) + 7; server = calloc(len, sizeof(char)); if(server != NULL) { snprintf(server, len, "%s#%u", ip, port); server[len - 1] = '\0'; } } // Extend reason with extra information (if available) char *reason_extended = (char *)reason; bool allocated = false; if(extra[0] != '\0') { const size_t len = strlen(reason) + strlen(extra) + 3; reason_extended = calloc(len, sizeof(char)); if(reason_extended != NULL) { snprintf(reason_extended, len, "%s%s", reason, extra); reason_extended[len - 1] = '\0'; allocated = true; } } // Log connection error log_connection_error(server, reason_extended, error); // Free allocated memory if(server != NULL) free(server); if(allocated) free(reason_extended); } // Restore errno for dnsmaq logging routines errno = errnum; } /** * @brief Retrieves the debug status of dnsmasq. * * @return true if the debug option is enabled, false otherwise. */ bool __attribute__ ((pure)) get_dnsmasq_debug(void) { return option_bool(OPT_DEBUG); } static bool enabled = false; /** * @brief Set the dnsmasq debug mode based on the enable flag and process ID. * * This function enables or disables the dnsmasq debug mode. When enabling, * it logs the process ID and name, sets the debug option, and marks the * debug mode as enabled. When disabling, it logs the detachment and clears * the debug option. If the debug mode is already enabled, it does nothing. * * @param enable A boolean flag indicating whether to enable or disable debug mode. * @param pid The process ID of the process to attach or detach the debugger. */ void set_dnsmasq_debug(const bool enable, const pid_t pid) { // Get debugger process' name char name[PROC_PATH_SIZ] = "???"; get_process_name(pid, name); // Only enable or disable if the debug mode is not already set if(enable && !get_dnsmasq_debug()) { // Enable debug mode log_info("Debugger attached (%d: %s), entering dnsmasq debug mode", pid, name); option_set(OPT_DEBUG); enabled = true; return; } else if(enabled) { // Disable debug mode log_info("Debugger detached, leaving dnsmasq debug mode"); option_clear(OPT_DEBUG); } }