FTL/src/resolve.c

682 lines
18 KiB
C

/* 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
* DNS Client Implementation
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
#include "FTL.h"
#include "resolve.h"
#include "shmem.h"
// struct config
#include "config.h"
// sleepms()
#include "timers.h"
// logg()
#include "log.h"
// global variable killed
#include "signals.h"
// getDatabaseHostname()
#include "database/network-table.h"
// struct _res
#include <resolv.h>
// resolveNetworkTableNames()
#include "database/network-table.h"
// resolver_ready
#include "daemon.h"
// logg_hostname_warning()
#include "database/message-table.h"
// Eventqueue routines
#include "events.h"
static bool res_initialized = false;
// Validate given hostname
static bool valid_hostname(char* name, const char* clientip)
{
// Check for validity of input
if(name == NULL)
return false;
// Check for maximum length of hostname
// Truncate if too long (MAXHOSTNAMELEN defaults to 64, see asm-generic/param.h)
if(strlen(name) > MAXHOSTNAMELEN)
{
logg("WARNING: Hostname of client %s too long, truncating to %d chars!",
clientip, MAXHOSTNAMELEN);
// We can modify the string in-place as the target is
// shorter than the source
name[MAXHOSTNAMELEN] = '\0';
}
// Iterate over characters in hostname
// to check for legal char: A-Z a-z 0-9 - _ .
unsigned int len = strlen(name);
for (unsigned int i = 0; i < len; i++)
{
const char c = name[i];
if ((c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_' ||
c == '.' )
continue;
// Invalid character found, log and return hostname being invalid
logg_hostname_warning(clientip, name, i);
return false;
}
// No invalid characters found
return true;
}
static void print_used_resolvers(const char *message)
{
logg("%s", message);
for(int i = 0u; i < 2*MAXNS; i++)
{
int family;
in_port_t port;
void *addr = NULL;
int j = i;
if(i < MAXNS)
{
// Regular name servers (IPv4)
// Some of the entries may not be configured
if(i > _res.nscount || _res.nsaddr_list[j].sin_family != AF_INET)
continue;
// IPv4 name servers
addr = &_res.nsaddr_list[j].sin_addr;
port = ntohs(_res.nsaddr_list[j].sin_port);
family = _res.nsaddr_list[j].sin_family;
}
else
{
// Extension name servers (IPv6)
j = i - MAXNS;
// Some of the entries may not be configured
if(_res._u._ext.nsaddrs[j] == NULL ||
_res._u._ext.nsaddrs[j]->sin6_family != AF_INET6)
continue;
addr = &_res._u._ext.nsaddrs[j]->sin6_addr;
port = ntohs(_res._u._ext.nsaddrs[j]->sin6_port);
family = _res._u._ext.nsaddrs[j]->sin6_family;
}
// Convert nameserver information to human-readable form
char nsname[INET6_ADDRSTRLEN];
inet_ntop(family, addr, nsname, INET6_ADDRSTRLEN);
logg(" %s %u: %s:%d (IPv%i)", i < MAXNS ? " " : "EXT",
j, nsname, port, family == AF_INET ? 4 : 6);
}
}
// Return if we want to resolve address to names at all
// (may be disabled due to config settings)
bool __attribute__((pure)) resolve_names(void)
{
if(!config.resolveIPv4 && !config.resolveIPv6)
return false;
return true;
}
// Return if we want to resolve this type of address to a name
bool __attribute__((pure)) resolve_this_name(const char *ipaddr)
{
if(!config.resolveIPv4 ||
(!config.resolveIPv6 && strstr(ipaddr,":") != NULL))
return false;
return true;
}
char *resolveHostname(const char *addr)
{
// Get host name
char *hostname = NULL;
if(config.debug & DEBUG_RESOLVER)
logg("Trying to resolve %s", addr);
// Check if this is a hidden client
// if so, return "hidden" as hostname
if(strcmp(addr, "0.0.0.0") == 0)
{
hostname = strdup("hidden");
if(config.debug & DEBUG_RESOLVER)
logg("---> \"%s\" (privacy settings)", hostname);
return hostname;
}
// Check if this is the internal client
// if so, return "hidden" as hostname
if(strcmp(addr, "::") == 0)
{
hostname = strdup("pi.hole");
if(config.debug & DEBUG_RESOLVER)
logg("---> \"%s\" (special)", hostname);
return hostname;
}
// Check if we want to resolve host names
if(!resolve_this_name(addr))
{
if(config.debug & DEBUG_RESOLVER)
logg("Configured to not resolve host name for %s", addr);
// Return an empty host name
return strdup("");
}
// Test if we want to resolve an IPv6 address
bool IPv6 = false;
if(strstr(addr,":") != NULL)
IPv6 = true;
// Convert address into binary form
struct sockaddr_storage ss = { 0 };
if(IPv6)
{
// Get binary form of IPv6 address
ss.ss_family = AF_INET6;
if(!inet_pton(ss.ss_family, addr, &(((struct sockaddr_in6 *)&ss)->sin6_addr)))
{
logg("WARN: Invalid IPv6 address when trying to resolve hostname: %s", addr);
return strdup("");
}
}
else
{
// Get binary form of IPv4 address
ss.ss_family = AF_INET;
if(!inet_pton(ss.ss_family, addr, &(((struct sockaddr_in *)&ss)->sin_addr)))
{
logg("WARN: Invalid IPv4 address when trying to resolve hostname: %s", addr);
return strdup("");
}
}
// Initialize resolver subroutines if trying to resolve for the first time
// res_init() reads resolv.conf to get the default domain name and name server
// address(es). If no server is given, the local host is tried. If no domain
// is given, that associated with the local host is used.
if(!res_initialized)
{
res_init();
res_initialized = true;
}
// INADDR_LOOPBACK is in host byte order, however, in_addr has to be in
// network byte order, convert it here if necessary
struct in_addr FTLaddr = { htonl(INADDR_LOOPBACK) };
in_port_t FTLport = htons(config.dns_port);
// Set FTL as system resolver only if not already the primary resolver
if(_res.nsaddr_list[0].sin_addr.s_addr != FTLaddr.s_addr || _res.nsaddr_list[0].sin_port != FTLport)
{
// Backup configured name servers and invalidate them
struct in_addr ns_addr_bck[MAXNS];
in_port_t ns_port_bck[MAXNS];
for(unsigned int i = 0u; i < MAXNS; i++)
{
ns_addr_bck[i] = _res.nsaddr_list[i].sin_addr;
ns_port_bck[i] = _res.nsaddr_list[i].sin_port;
_res.nsaddr_list[i].sin_addr.s_addr = 0; // 0.0.0.0
}
// Set FTL at 127.0.0.1 as the only resolver
_res.nsaddr_list[0].sin_addr.s_addr = FTLaddr.s_addr;
// Set resolver port
_res.nsaddr_list[0].sin_port = FTLport;
if(config.debug & DEBUG_RESOLVER)
print_used_resolvers("Setting nameservers to:");
// Try to resolve address
char host[NI_MAXHOST] = { 0 };
int ret = getnameinfo((struct sockaddr*)&ss, sizeof(ss), host, sizeof(host), NULL, 0, NI_NAMEREQD);
// Check if getnameinfo() returned a host name
if(ret == 0)
{
if(valid_hostname(host, addr))
{
// Return hostname copied to new memory location
hostname = strdup(host);
}
else
{
hostname = strdup("[invalid host name]");
}
if(config.debug & DEBUG_RESOLVER)
logg(" ---> \"%s\" (found internally)", hostname);
}
else if(config.debug & DEBUG_RESOLVER)
{
logg(" ---> \"\" (not found internally: %s", gai_strerror(ret));
}
// Restore resolvers (without forced FTL)
for(unsigned int i = 0u; i < MAXNS; i++)
{
_res.nsaddr_list[i].sin_addr = ns_addr_bck[i];
_res.nsaddr_list[i].sin_port = ns_port_bck[i];
}
if(config.debug & DEBUG_RESOLVER)
print_used_resolvers("Setting nameservers back to default:");
}
else if(config.debug & DEBUG_RESOLVER)
print_used_resolvers("FTL already primary nameserver:");
// If no host name was found before, try again with system-configured
// resolvers (necessary for docker and friends)
if(hostname == NULL)
{
// Try to resolve address
char host[NI_MAXHOST] = { 0 };
int ret = getnameinfo((struct sockaddr*)&ss, sizeof(ss), host, sizeof(host), NULL, 0, NI_NAMEREQD);
// Check if getnameinfo() returned a host name this time
// First check for he not being NULL before trying to dereference it
if(ret == 0)
{
if(valid_hostname(host, addr))
{
// Return hostname copied to new memory location
hostname = strdup(host);
}
else
{
hostname = strdup("[invalid host name]");
}
if(config.debug & DEBUG_RESOLVER)
logg(" ---> \"%s\" (found externally)", hostname);
}
else
{
// No hostname found (empty PTR)
hostname = strdup("");
if(config.debug & DEBUG_RESOLVER)
{
logg(" ---> \"\" (not found externally: %s)", gai_strerror(ret));
}
}
}
// Return result
return hostname;
}
// Resolve upstream destination host names
static size_t resolveAndAddHostname(size_t ippos, size_t oldnamepos)
{
// Get IP and host name strings. They are cloned in case shared memory is
// resized before the next lock
lock_shm();
char *ipaddr = strdup(getstr(ippos));
char *oldname = strdup(getstr(oldnamepos));
unlock_shm();
// Test if we want to resolve host names, otherwise all calls to resolveHostname()
// and getNameFromIP() can be skipped as they will all return empty names (= no records)
if(!resolve_this_name(ipaddr))
{
if(config.debug & DEBUG_RESOLVER)
logg(" ---> \"\" (configured to not resolve host name)");
// Free allocated memory
free(ipaddr);
free(oldname);
// Return fixed position of empty string
return 0;
}
// Important: Don't hold a lock while resolving as the main thread
// (dnsmasq) needs to be operable during the call to resolveHostname()
char *newname = resolveHostname(ipaddr);
// If no hostname was found, try to obtain hostname from the network table
// This may be disabled due to a user setting
if(strlen(newname) == 0 && config.names_from_netdb)
{
free(newname);
newname = getNameFromIP(NULL, ipaddr);
if(newname != NULL && config.debug & DEBUG_RESOLVER)
logg(" ---> \"%s\" (provided by database)", newname);
}
// Only store new newname if it is valid and differs from oldname
// We do not need to check for oldname == NULL as names are
// always initialized with an empty string at position 0
if(newname != NULL && strcmp(oldname, newname) != 0)
{
lock_shm();
size_t newnamepos = addstr(newname);
// newname has already been checked against NULL
// so we can safely free it
free(newname);
free(ipaddr);
free(oldname);
unlock_shm();
return newnamepos;
}
else if(config.debug & DEBUG_SHMEM)
{
// Debugging output
logg("Not adding \"%s\" to buffer (unchanged)", oldname);
}
if(newname != NULL)
free(newname);
free(ipaddr);
free(oldname);
// Not changed, return old namepos
return oldnamepos;
}
// Resolve client host names
static void resolveClients(const bool onlynew, const bool force_refreshing)
{
const time_t now = time(NULL);
// Lock counter access here, we use a copy in the following loop
lock_shm();
int clientscount = counters->clients;
unlock_shm();
int skipped = 0;
for(int clientID = 0; clientID < clientscount; clientID++)
{
// Memory access needs to get locked
lock_shm();
// Get client pointer for the first time (reading data)
clientsData* client = getClient(clientID, true);
if(client == NULL)
{
logg("ERROR: Unable to get client pointer (1) with ID %i, skipping...", clientID);
skipped++;
unlock_shm();
continue;
}
// Skip alias-clients
if(client->flags.aliasclient)
{
unlock_shm();
continue;
}
bool newflag = client->flags.new;
size_t ippos = client->ippos;
size_t oldnamepos = client->namepos;
// Only try to resolve host names of clients which were recently active if we are re-resolving
// Limit for a "recently active" client is two hours ago
if(!force_refreshing && !onlynew && client->lastQuery < now - 2*60*60)
{
if(config.debug & DEBUG_RESOLVER)
{
logg("Skipping client %s (%s) because it was inactive for %i seconds",
getstr(ippos), getstr(oldnamepos), (int)(now - client->lastQuery));
}
unlock_shm();
continue;
}
unlock_shm();
// If onlynew flag is set, we will only resolve new clients
// If not, we will try to re-resolve all known clients
if(!force_refreshing && onlynew && !newflag)
{
if(config.debug & DEBUG_RESOLVER)
{
logg("Skipping client %s (%s) because it is not new",
getstr(ippos), getstr(oldnamepos));
}
skipped++;
continue;
}
// Check if we want to resolve an IPv6 address
bool IPv6 = false;
const char *ipaddr = NULL;
if((ipaddr = getstr(ippos)) != NULL && strstr(ipaddr,":") != NULL)
IPv6 = true;
// If we're in refreshing mode (onlynew == false), we skip clients if
// 1. We should not refresh any hostnames
// 2. We should only refresh IPv4 client, but this client is IPv6
// 3. We should only refresh unknown hostnames, but leave
// existing ones as they are
if(onlynew == false &&
(config.refresh_hostnames == REFRESH_NONE ||
(config.refresh_hostnames == REFRESH_IPV4_ONLY && IPv6) ||
(config.refresh_hostnames == REFRESH_UNKNOWN && oldnamepos != 0)))
{
if(config.debug & DEBUG_RESOLVER)
{
const char *reason = "N/A";
if(config.refresh_hostnames == REFRESH_NONE)
reason = "Not refreshing any hostnames";
else if(config.refresh_hostnames == REFRESH_IPV4_ONLY)
reason = "Only refreshing IPv4 names";
else if(config.refresh_hostnames == REFRESH_UNKNOWN)
reason = "Looking only for unknown hostnames";
logg("Skipping client %s (%s) because it should not be refreshed: %s",
getstr(ippos), getstr(oldnamepos), reason);
}
skipped++;
if(config.debug & DEBUG_RESOLVER)
{
lock_shm();
logg("Client %s -> \"%s\" already known", getstr(ippos), getstr(oldnamepos));
unlock_shm();
}
continue;
}
// Obtain/update hostname of this client
size_t newnamepos = resolveAndAddHostname(ippos, oldnamepos);
lock_shm();
// Get client pointer for the second time (writing data)
// We cannot use the same pointer again as we released
// the lock in between so we cannot know if something
// happened to the shared memory object (resize event)
client = getClient(clientID, true);
if(client == NULL)
{
logg("ERROR: Unable to get client pointer (2) with ID %i, skipping...", clientID);
skipped++;
unlock_shm();
continue;
}
// Store obtained host name (may be unchanged)
client->namepos = newnamepos;
// Mark entry as not new
client->flags.new = false;
if(config.debug & DEBUG_RESOLVER)
logg("Client %s -> \"%s\" is new", getstr(ippos), getstr(newnamepos));
unlock_shm();
}
if(config.debug & DEBUG_RESOLVER)
{
logg("%i / %i client host names resolved",
clientscount-skipped, clientscount);
}
}
// Resolve upstream destination host names
static void resolveUpstreams(const bool onlynew)
{
const time_t now = time(NULL);
// Lock counter access here, we use a copy in the following loop
lock_shm();
int upstreams = counters->upstreams;
unlock_shm();
int skipped = 0;
for(int upstreamID = 0; upstreamID < upstreams; upstreamID++)
{
// Memory access needs to get locked
lock_shm();
// Get upstream pointer for the first time (reading data)
upstreamsData* upstream = getUpstream(upstreamID, true);
if(upstream == NULL)
{
logg("ERROR: Unable to get upstream pointer with ID %i, skipping...", upstreamID);
skipped++;
unlock_shm();
continue;
}
bool newflag = upstream->new;
size_t ippos = upstream->ippos;
size_t oldnamepos = upstream->namepos;
// Only try to resolve host names of upstream servers which were recently active
// Limit for a "recently active" upstream server is two hours ago
if(upstream->lastQuery < now - 2*60*60)
{
if(config.debug & DEBUG_RESOLVER)
{
logg("Skipping upstream %s (%s) because it was inactive for %i seconds",
getstr(ippos), getstr(oldnamepos), (int)(now - upstream->lastQuery));
}
unlock_shm();
continue;
}
unlock_shm();
// If onlynew flag is set, we will only resolve new upstream destinations
// If not, we will try to re-resolve all known upstream destinations
if(onlynew && !newflag)
{
skipped++;
if(config.debug & DEBUG_RESOLVER)
{
lock_shm();
logg("Upstream %s -> \"%s\" already known", getstr(ippos), getstr(oldnamepos));
unlock_shm();
}
continue;
}
// Obtain/update hostname of this client
size_t newnamepos = resolveAndAddHostname(ippos, oldnamepos);
lock_shm();
// Get upstream pointer for the second time (writing data)
// We cannot use the same pointer again as we released
// the lock in between so we cannot know if something
// happened to the shared memory object (resize event)
upstream = getUpstream(upstreamID, true);
if(upstream == NULL)
{
logg("ERROR: Unable to get upstream pointer with ID %i, skipping...", upstreamID);
skipped++;
unlock_shm();
continue;
}
// Store obtained host name (may be unchanged)
upstream->namepos = newnamepos;
// Mark entry as not new
upstream->new = false;
if(config.debug & DEBUG_RESOLVER)
logg("Upstream %s -> \"%s\" is new", getstr(ippos), getstr(newnamepos));
unlock_shm();
}
if(config.debug & DEBUG_RESOLVER)
{
logg("%i / %i upstream server host names resolved",
upstreams-skipped, upstreams);
}
}
void *DNSclient_thread(void *val)
{
// Set thread name
thread_names[DNSclient] = "DNS client";
prctl(PR_SET_NAME, thread_names[DNSclient], 0, 0, 0);
// Initial delay until we first try to resolve anything
thread_sleepms(DNSclient, 2000);
// Run as long as this thread is not canceled
while(!killed)
{
// Run whenever necessary to resolve only new clients and
// upstream servers
if(resolver_ready && get_and_clear_event(RESOLVE_NEW_HOSTNAMES))
{
// Try to resolve new client host names
// (onlynew=true)
// We're not forcing refreshing here
resolveClients(true, false);
// Try to resolve new upstream destination host names
// (onlynew=true)
resolveUpstreams(true);
}
// Intermediate cancellation-point
if(killed)
break;
// Run every hour to update possibly changed client host names
if(resolver_ready && (time(NULL) % RERESOLVE_INTERVAL == 0))
{
set_event(RERESOLVE_HOSTNAMES); // done below
}
bool force_refreshing = false;
if(get_and_clear_event(RERESOLVE_HOSTNAMES_FORCE))
{
set_event(RERESOLVE_HOSTNAMES); // done below
force_refreshing = true;
}
// Process resolver related event queue elements
if(get_and_clear_event(RERESOLVE_HOSTNAMES))
{
// Try to resolve all client host names
// (onlynew=false)
resolveClients(false, force_refreshing);
// Intermediate cancellation-point
if(killed)
break;
// Try to resolve all upstream destination host names
// (onlynew=false)
resolveUpstreams(false);
}
// Idle for 1 sec
thread_sleepms(DNSclient, 1000);
}
logg("Terminating resolver thread");
return NULL;
}