From c2d4f068643043edba376d2b9799b38652ecd08a Mon Sep 17 00:00:00 2001 From: DL6ER Date: Fri, 15 Dec 2023 20:45:25 +0100 Subject: [PATCH 1/5] Implement IP -> hostname resolver Signed-off-by: DL6ER --- src/args.c | 22 +++ src/resolve.c | 494 +++++++++++++++++++++++++++++++------------------- src/resolve.h | 2 +- 3 files changed, 334 insertions(+), 184 deletions(-) diff --git a/src/args.c b/src/args.c index 0d3d28d33..4feaed770 100644 --- a/src/args.c +++ b/src/args.c @@ -64,6 +64,8 @@ #include // sha256sum() #include "files.h" +// resolveHostname() +#include "resolve.h" // defined in dnsmasq.c extern void print_dnsmasq_version(const char *yellow, const char *green, const char *bold, const char *normal); @@ -496,6 +498,25 @@ void parse_args(int argc, char* argv[]) exit(EXIT_SUCCESS); } + // Local reverse name resolver + if(argc == 3 && strcasecmp(argv[1], "ptr") == 0) + { + // Enable stdout printing + cli_mode = true; + + // Need to get dns.port and the resolver settings + readFTLconf(&config, false); + + char *name = resolveHostname(argv[2]); + if(name == NULL) + exit(EXIT_FAILURE); + + // Print result + printf("%s -> %s\n", argv[2], name); + free(name); + exit(EXIT_SUCCESS); + } + // start from 1, as argv[0] is the executable name for(int i = 1; i < argc; i++) { @@ -980,6 +1001,7 @@ void parse_args(int argc, char* argv[]) printf(" Decoding: %spihole-FTL idn2 -d %spunycode%s\n\n", green, cyan, normal); printf("%sOther:%s\n", yellow, normal); + printf("\t%sptr %sIP%s Resolve IP address to hostname\n", green, cyan, normal); printf("\t%ssha256sum %sfile%s Calculate SHA256 checksum of a file\n", green, cyan, normal); printf("\t%sdhcp-discover%s Discover DHCP servers in the local\n", green, normal); printf("\t network\n"); diff --git a/src/resolve.c b/src/resolve.c index 0d2a18603..ad8462a0f 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -32,7 +32,56 @@ // resolve_regex_cnames() #include "regex_r.h" -static bool res_initialized = false; +// Function Prototypes +static void name_toDNS(unsigned char *dns, const size_t dnslen, const char *host, const size_t hostlen) __attribute__((nonnull(1,3))); +static unsigned char *name_fromDNS(unsigned char *reader, unsigned char *buffer, uint16_t *count) __attribute__((malloc)) __attribute__((nonnull(1,2,3))); + +// DNS header structure +struct DNS_HEADER +{ + uint16_t id; // identification number + + bool rd :1; // recursion desired + bool tc :1; // truncated message + bool aa :1; // authoritive answer + uint8_t opcode :4; // purpose of message + bool qr :1; // query/response flag + + uint8_t rcode :4; // response code + bool cd :1; // checking disabled + bool ad :1; // authenticated data + bool z :1; // its z! reserved + bool ra :1; // recursion available + + uint16_t q_count; // number of question entries + uint16_t ans_count; // number of answer entries + uint16_t auth_count; // number of authority entries + uint16_t add_count; // number of resource entries +} __attribute__((packed)); + +// Constant sized fields of query structure +struct QUESTION +{ + uint16_t qtype; + uint16_t qclass; +}; + +// Constant sized fields of the resource record structure +struct R_DATA +{ + uint16_t type; + uint16_t class; + uint32_t ttl; // RFC 1035 defines it as positive values of a signed 32bit number + uint16_t data_len; +} __attribute__((packed)); + +// Pointers to resource record contents +struct RES_RECORD +{ + unsigned char *name; + struct R_DATA *resource; + uint8_t *rdata; +}; // Validate given hostname static bool valid_hostname(char* name, const char* clientip) @@ -75,75 +124,232 @@ static bool valid_hostname(char* name, const char* clientip) return true; } -#define NUM_NS4 (sizeof(_res.nsaddr_list) / sizeof(_res.nsaddr_list[0])) -#define NUM_NS6 (sizeof(_res._u._ext.nsaddrs) / sizeof(_res._u._ext.nsaddrs[0])) +// 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.resolver.resolveIPv4.v.b && !config.resolver.resolveIPv6.v.b) + 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.resolver.resolveIPv4.v.b || + (!config.resolver.resolveIPv6.v.b && strstr(ipaddr,":") != NULL)) + return false; + return true; +} -static void print_used_resolvers(const char *message) +// Perform a name lookup by sending a packet to ourselves +static char *__attribute__((malloc)) ngethostbyname(const char *host, const char *ipaddr) { - // Print details only when debugging - if(!(config.debug.resolver.v.b)) - return; + uint8_t buf[1024] = { 0 }; + uint8_t *qname = NULL, *reader = NULL; + struct RES_RECORD answers[20] = { 0 }; // buffer for DNS replies + struct DNS_HEADER *dns = NULL; + struct QUESTION *qinfo = NULL; + + // Set the DNS structure to standard queries + dns = (struct DNS_HEADER *)&buf; + dns->id = (unsigned short) htons(random()); // random query ID + dns->qr = 0; // This is a query + dns->opcode = 0; // This is a standard query + dns->aa = 0; // Not Authoritative + dns->tc = 0; // This message is not truncated + dns->rd = 1; // Recursion Desired + dns->ra = 0; // Recursion not available! + dns->z = 0; // Reserved + dns->ad = 0; // This is not an authenticated answer + dns->cd = 0; // Checking Disabled + dns->rcode = 0; // Response code + dns->q_count = htons(1); // 1 question + dns->ans_count = 0; // No answers + dns->auth_count = 0; // No authority + dns->add_count = 0; // No additional + + // Point to the query portion + qname = &buf[sizeof(struct DNS_HEADER)]; + + // Make a copy of the hostname with two extra bytes for the length and + // the final dot, copy the hostname into it and convert to convert to + // DNS format + const size_t hnamelen = strlen(host) + 2; + char *hname = calloc(hnamelen, sizeof(char)); + if(hname == NULL) + { + log_err("Unable to allocate memory for hname"); + return strdup(""); + } + strncpy(hname, host, hnamelen); + strncat(hname, ".", hnamelen - strlen(hname)); + hname[hnamelen - 1] = '\0'; + + name_toDNS(qname, sizeof(buf) - sizeof(struct DNS_HEADER), hname, hnamelen); + free(hname); + qinfo = (void*)&buf[sizeof(struct DNS_HEADER) + (strlen((const char*)qname) + 1)]; + + qinfo->qtype = htons(T_PTR); // Type of the query, A, MX, CNAME, NS etc + qinfo->qclass = htons(1); // IN + + // UDP packet for DNS queries + const int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + struct sockaddr_in dest = { 0 }; + dest.sin_family = AF_INET; // IPv4 + dest.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1 + dest.sin_port = htons(config.dns.port.v.u16); // Configured DNS port + + const size_t questionlen = sizeof(struct DNS_HEADER) + (strlen((const char*)qname) + 1) + sizeof(struct QUESTION); + if(sendto(s, buf, questionlen, 0, (struct sockaddr*)&dest, sizeof(dest)) < 0) + { + perror("sendto failed"); + close(s); + return strdup(""); + } + + // Receive the answer + socklen_t addrlen = sizeof(dest); + if(recvfrom (s, buf, sizeof(buf), 0, (struct sockaddr*)&dest, &addrlen) < 0) + { + perror("recvfrom failed"); + close(s); + return strdup(""); + } + + // Close socket + close(s); - log_debug(DEBUG_RESOLVER, "%s", message); - for(unsigned int i = 0u; i < NUM_NS4 + NUM_NS6; i++) + // Parse the reply + dns = (struct DNS_HEADER*) buf; + // Move ahead of the dns header and the query field + reader = &buf[questionlen]; + + // Start reading answers + uint16_t stop = 0; + char *name = NULL; + for(uint16_t i = 0; i < ntohs(dns->ans_count); i++) { - int family; - in_port_t port; - void *addr = NULL; - if(i < NUM_NS4) + answers[i].name = name_fromDNS(reader, buf, &stop); + reader = reader + stop; + + answers[i].resource = (struct R_DATA*)(reader); + reader = reader + sizeof(struct R_DATA); + + // We only care about PTR answers and ignore all others + if(ntohs(answers[i].resource->type) != T_PTR) + continue; + + // Read the answer and convert from network to host representation + answers[i].rdata = name_fromDNS(reader, buf, &stop); + reader = reader + stop; + + name = (char *)answers[i].rdata; + log_debug(DEBUG_RESOLVER, "Resolving %s (PTR \"%s\"): %u = \"%s\"", + ipaddr, answers[i].name, i, answers[i].rdata); + + // We break out of the loop if this is a valid hostname + if(valid_hostname(name, ipaddr)) { - // Regular name servers (IPv4) - const unsigned int j = i; - // Some of the entries may not be configured - if(_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; + break; } else { - // Extension name servers (IPv6) - const unsigned int j = i - NUM_NS4; - // 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; + // Discard this answer: free memory and set name to NULL + free(name); + name = NULL; } + } - // Convert nameserver information to human-readable form - char nsname[INET6_ADDRSTRLEN]; - inet_ntop(family, addr, nsname, INET6_ADDRSTRLEN); - - log_debug(DEBUG_RESOLVER, " %s %u: %s:%hu (IPv%u)", i < MAXNS ? " " : "EXT", - i, nsname, port, family == AF_INET ? 4u : 6u); + if(name != NULL) + { + // We have a valid hostname, return it + // This is allocated memory + return name; + } + else + { + // No valid hostname found, return empty string + return strdup(""); } } -// Return if we want to resolve address to names at all -// (may be disabled due to config settings) -bool __attribute__((pure)) resolve_names(void) +// Convert hostname from network to host representation +// 3www6google3com -> www.google.com +static u_char * __attribute__((malloc)) __attribute__((nonnull(1,2,3))) name_fromDNS(unsigned char *reader, unsigned char *buffer, uint16_t *count) { - if(!config.resolver.resolveIPv4.v.b && !config.resolver.resolveIPv6.v.b) - return false; - return true; + unsigned char *name = calloc(MAXHOSTNAMELEN, sizeof(char)); + unsigned int p = 0, jumped = 0, offset = 0; + + *count = 1; + + // Parse DNS label string encoding (e.g, 3www6google3com) + while(*reader!= 0) + { + if(*reader >= 192) + { + offset = (*reader)*256 + *(reader + 1) - 49152; // 49152 = 11000000 00000000 + reader = buffer + offset - 1; + jumped = 1; // We have jumped to another location so counting won't go up + } + else + name[p++] = *reader; + reader = reader + 1; + + if(jumped == 0) + *count = *count + 1; // If we haven't jumped to another location then we can count up + } + + // Terminate string + name[p] = '\0'; + + // Number of steps we actually moved forward in the packet + if(jumped == 1) + *count += 1; + + // now convert 3www6google3com0 to www.google.com + unsigned int i = 0; + for(; i < strlen((const char*)name); i++) + { + p=name[i]; + for(unsigned j = 0; j < p; j++) + { + name[i]=name[i+1]; + i=i+1; + } + name[i]='.'; + } + + // Strip off the trailing dot + name[i-1] = '\0'; + return name; } -// Return if we want to resolve this type of address to a name -bool __attribute__((pure)) resolve_this_name(const char *ipaddr) +// Convert hostname from host to network representation +// www.google.com -> 3www6google3com +static void __attribute__((nonnull(1,3))) name_toDNS(unsigned char *dns, const size_t dnslen, const char *host, const size_t hostlen) { - if(!config.resolver.resolveIPv4.v.b || - (!config.resolver.resolveIPv6.v.b && strstr(ipaddr,":") != NULL)) - return false; - return true; + unsigned int lock = 0; + + // Iterate over hostname characters + for(unsigned int i = 0 ; i < strlen((char*)host); i++) + { + // If we encounter a dot, write the number of characters since the last dot + // and then write the characters themselves + if(host[i] == '.') + { + *dns++ = i - lock; + for(;lock < i; lock++) + *dns++ = host[lock]; + lock++; + } + } + + // Terminate the string at the end + *dns++='\0'; } -char *resolveHostname(const char *addr) +char *__attribute__((malloc)) resolveHostname(const char *addr) { // Get host name char *hostn = NULL; @@ -193,6 +399,7 @@ char *resolveHostname(const char *addr) // Convert address into binary form struct sockaddr_storage ss = { 0 }; + char *inaddr = NULL; if(IPv6) { // Get binary form of IPv6 address @@ -202,158 +409,79 @@ char *resolveHostname(const char *addr) log_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))) + + // Need extra space for ".ip6.arpa" suffix + // The 1.2.3.4... string is 63 + terminating \0 = 64 bytes long + inaddr = calloc(64 + 10, sizeof(char)); + if(inaddr == NULL) { - log_warn("Invalid IPv4 address when trying to resolve hostname: %s", addr); + log_err("Unable to allocate memory for reverse lookup"); 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.v.u16); - - // Temporarily set FTL as system resolver - - // Backup configured name servers and invalidate them (IPv4) - struct in_addr ns_addr_bck[MAXNS]; - in_port_t ns_port_bck[MAXNS]; - int bck_nscount = _res.nscount; - for(unsigned int i = 0u; i < NUM_NS4; 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 - } + // Convert IPv6 address to reverse lookup format + // b a 9 8 7 6 5 4 |<-- :: -->| 0 1 2 3 4 + // 4321:0::4:567:89ab -> b.a.9.8.7.6.5.0.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.2.3.4 + for(int i = 0; i < 32; i++) + { + // Get current nibble + uint8_t nibble = ((uint8_t *)&(((struct sockaddr_in6 *)&ss)->sin6_addr))[i/2]; - // 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; - // Configure resolver to use only one resolver - _res.nscount = 1; - - // Backup configured name server and invalidate them (IPv6) - struct in6_addr ns6_addr_bck[MAXNS]; - in_port_t ns6_port_bck[MAXNS]; - int bck_nscount6 = _res._u._ext.nscount6; - for(unsigned int i = 0u; i < NUM_NS6; i++) - { - if(_res._u._ext.nsaddrs[i] == NULL) - continue; - memcpy(&ns6_addr_bck[i], &_res._u._ext.nsaddrs[i]->sin6_addr, sizeof(struct in6_addr)); - ns6_port_bck[i] = _res._u._ext.nsaddrs[i]->sin6_port; - memcpy(&_res._u._ext.nsaddrs[i]->sin6_addr, &in6addr_any, sizeof(struct in6_addr)); - } + // Get lower nibble for even i, upper nibble for odd i + if(i % 2 == 0) + nibble = nibble >> 4; - // Set FTL as the only resolver only when IPv6 is enabled - if(bck_nscount6 > 0) - { - // Set FTL at ::1 as the only resolver - memcpy(&_res._u._ext.nsaddrs[0]->sin6_addr, &in6addr_loopback, sizeof(struct in6_addr)); - // Set resolver port - _res._u._ext.nsaddrs[0]->sin6_port = FTLport; - // Configure resolver to use only one resolver - _res._u._ext.nscount6 = 1; - } + // Mask out upper nibble + nibble = nibble & 0x0F; - if(config.debug.resolver.v.b) - print_used_resolvers("Set nameservers to:"); + // Convert to ASCII + char c = '0' + nibble; + if(c > '9') + c = 'a' + nibble - 10; - // Try to resolve address - char host[NI_MAXHOST] = { 0 }; - int ret = getnameinfo((struct sockaddr*)&ss, sizeof(ss), host, sizeof(host), NULL, 0, NI_NAMEREQD); + // Prepend to string + inaddr[62-2*i] = c; - // Check if getnameinfo() returned a host name - if(ret == 0) - { - if(valid_hostname(host, addr)) - { - // Return hostname copied to new memory location - hostn = strdup(host); - } - else - { - hostn = strdup("[invalid host name]"); + // Add dot after (actually: before) every nibble except + // the last one + if(i != 31) + inaddr[62-2*i-1] = '.'; } - log_debug(DEBUG_RESOLVER, " ---> \"%s\" (found internally)", hostn); + // Add suffix + strcat(inaddr, ".ip6.arpa"); } else - log_debug(DEBUG_RESOLVER, " ---> \"\" (not found internally: %s", gai_strerror(ret)); - - // Restore IPv4 resolvers - for(unsigned int i = 0u; i < NUM_NS4; i++) - { - _res.nsaddr_list[i].sin_addr = ns_addr_bck[i]; - _res.nsaddr_list[i].sin_port = ns_port_bck[i]; - } - _res.nscount = bck_nscount; - - // Restore IPv6 resolvers - for(unsigned int i = 0u; i < NUM_NS6; i++) - { - if(_res._u._ext.nsaddrs[i] == NULL) - continue; - memcpy(&_res._u._ext.nsaddrs[i]->sin6_addr, &ns6_addr_bck[i], sizeof(struct in6_addr)); - _res._u._ext.nsaddrs[i]->sin6_port = ns6_port_bck[i]; - } - _res._u._ext.nscount6 = bck_nscount6; - - if(config.debug.resolver.v.b) - print_used_resolvers("Restored nameservers to:"); - - // If no host name was found before, try again with system-configured - // resolvers (necessary for docker and friends) - if(hostn == NULL) { - // Try to resolve address - 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) + // Get binary form of IPv4 address + ss.ss_family = AF_INET; + if(!inet_pton(ss.ss_family, addr, &(((struct sockaddr_in *)&ss)->sin_addr))) { - if(valid_hostname(host, addr)) - { - // Return hostname copied to new memory location - hostn = strdup(host); - } - else - { - hostn = strdup("[invalid host name]"); - } - - log_debug(DEBUG_RESOLVER, " ---> \"%s\" (found externally)", hostn); + log_warn("Invalid IPv4 address when trying to resolve hostname: %s", addr); + return strdup(""); } - else - { - // No hostname found (empty PTR) - hostn = strdup(""); - if(config.debug.resolver.v.b) - log_debug(DEBUG_RESOLVER, " ---> \"\" (not found externally: %s)", gai_strerror(ret)); + // Need extra space for ".in-addr.arpa" suffix + inaddr = calloc(INET_ADDRSTRLEN + 14, sizeof(char)); + if(inaddr == NULL) + { + log_err("Unable to allocate memory for reverse lookup"); + return strdup(""); } + + // Convert IPv4 address to reverse lookup format + // 12.34.56.78 -> 78.56.34.12.in-addr.arpa + snprintf(inaddr, INET_ADDRSTRLEN + 14, "%d.%d.%d.%d.in-addr.arpa", + (int)((uint8_t *)&(((struct sockaddr_in *)&ss)->sin_addr))[3], + (int)((uint8_t *)&(((struct sockaddr_in *)&ss)->sin_addr))[2], + (int)((uint8_t *)&(((struct sockaddr_in *)&ss)->sin_addr))[1], + (int)((uint8_t *)&(((struct sockaddr_in *)&ss)->sin_addr))[0]); } - // Return result - return hostn; + // Get host name by making a reverse lookup to ourselves (server at 127.0.0.1 with port 53) + // We implement a minimalistic resolver here as we cannot rely on the system resolver using whatever + // nameserver we configured in /etc/resolv.conf + return ngethostbyname(inaddr, addr); } // Resolve upstream destination host names diff --git a/src/resolve.h b/src/resolve.h index 6598faa9e..154c7c7bc 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -11,7 +11,7 @@ #define RESOLVE_H void *DNSclient_thread(void *val); -char *resolveHostname(const char *addr); +char *resolveHostname(const char *addr) __attribute__((malloc)); bool resolve_names(void) __attribute__((pure)); bool resolve_this_name(const char *ipaddr) __attribute__((pure)); From 6bc033e537b80d21c4f9ecebda190b59ea18caf8 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Fri, 15 Dec 2023 20:56:00 +0100 Subject: [PATCH 2/5] Add CI tests Signed-off-by: DL6ER --- src/args.c | 4 ++-- src/database/network-table.c | 2 +- src/resolve.c | 17 ++++------------- src/resolve.h | 2 +- test/test_suite.bats | 9 +++++++++ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/args.c b/src/args.c index 4feaed770..29b24e2f2 100644 --- a/src/args.c +++ b/src/args.c @@ -507,12 +507,12 @@ void parse_args(int argc, char* argv[]) // Need to get dns.port and the resolver settings readFTLconf(&config, false); - char *name = resolveHostname(argv[2]); + char *name = resolveHostname(argv[2], true); if(name == NULL) exit(EXIT_FAILURE); // Print result - printf("%s -> %s\n", argv[2], name); + printf("%s\n", name); free(name); exit(EXIT_SUCCESS); } diff --git a/src/database/network-table.c b/src/database/network-table.c index 8c0e9a1fe..b9da8068e 100644 --- a/src/database/network-table.c +++ b/src/database/network-table.c @@ -19,7 +19,7 @@ #include "../datastructure.h" // struct config #include "../config/config.h" -// resolveHostname() +// resolve_this_name() #include "../resolve.h" // killed #include "../signals.h" diff --git a/src/resolve.c b/src/resolve.c index ad8462a0f..11d710b1b 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -43,7 +43,7 @@ struct DNS_HEADER bool rd :1; // recursion desired bool tc :1; // truncated message - bool aa :1; // authoritive answer + bool aa :1; // authoritative answer uint8_t opcode :4; // purpose of message bool qr :1; // query/response flag @@ -349,13 +349,13 @@ static void __attribute__((nonnull(1,3))) name_toDNS(unsigned char *dns, const s *dns++='\0'; } -char *__attribute__((malloc)) resolveHostname(const char *addr) +char *__attribute__((malloc)) resolveHostname(const char *addr, const bool force) { // Get host name char *hostn = NULL; // Check if we want to resolve host names - if(!resolve_this_name(addr)) + if(!force && !resolve_this_name(addr)) { log_debug(DEBUG_RESOLVER, "Configured to not resolve host name for %s", addr); @@ -383,15 +383,6 @@ char *__attribute__((malloc)) resolveHostname(const char *addr) return hostn; } - // Check if we want to resolve host names - if(!resolve_this_name(addr)) - { - log_debug(DEBUG_RESOLVER, "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) @@ -510,7 +501,7 @@ static size_t resolveAndAddHostname(size_t ippos, size_t oldnamepos) // 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); + char *newname = resolveHostname(ipaddr, false); // If no hostname was found, try to obtain hostname from the network table // This may be disabled due to a user setting diff --git a/src/resolve.h b/src/resolve.h index 154c7c7bc..2dfc50e1a 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -11,7 +11,7 @@ #define RESOLVE_H void *DNSclient_thread(void *val); -char *resolveHostname(const char *addr) __attribute__((malloc)); +char *resolveHostname(const char *addr, const bool force) __attribute__((malloc)); bool resolve_names(void) __attribute__((pure)); bool resolve_this_name(const char *ipaddr) __attribute__((pure)); diff --git a/test/test_suite.bats b/test/test_suite.bats index 0ea50189c..d341a0800 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1570,6 +1570,15 @@ [[ ${lines[0]} == "eae293f0c30369935a7457a789658bedebf92d544e7526bc43aa07883a597fa9 test/test.pem" ]] } +@test "Internal IP -> name resolution works" { + run bash -c "./pihole-FTL ptr 127.0.0.1 | tail -n1" + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "localhost" ]] + run bash -c "./pihole-FTL ptr ::1 | tail -n1" + printf "%s\n" "${lines[@]}" + [[ ${lines[0]} == "localhost" ]] +} + @test "API validation" { run python3 test/api/checkAPI.py printf "%s\n" "${lines[@]}" From 48184ad6df646c94eb63796541e5afb0d5b08e50 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Sat, 16 Dec 2023 19:51:36 +0100 Subject: [PATCH 3/5] Add static assertion for DNS struct sizes ans work aroung gcc bug on arm32 issusing a warning where there should be none Signed-off-by: DL6ER --- src/resolve.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/resolve.c b/src/resolve.c index 11d710b1b..7b56f397e 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -31,11 +31,19 @@ #include "events.h" // resolve_regex_cnames() #include "regex_r.h" +// statis_assert() +#include // Function Prototypes static void name_toDNS(unsigned char *dns, const size_t dnslen, const char *host, const size_t hostlen) __attribute__((nonnull(1,3))); static unsigned char *name_fromDNS(unsigned char *reader, unsigned char *buffer, uint16_t *count) __attribute__((malloc)) __attribute__((nonnull(1,2,3))); +// Avoid "error: packed attribute causes inefficient alignment for ..." on ARM32 +// builds due to the use of __attribute__((packed)) in the following structs +// Their correct size is ensured for each by static_assert() below +_Pragma("GCC diagnostic push") +_Pragma("GCC diagnostic ignored \"-Wattributes\"") + // DNS header structure struct DNS_HEADER { @@ -58,6 +66,7 @@ struct DNS_HEADER uint16_t auth_count; // number of authority entries uint16_t add_count; // number of resource entries } __attribute__((packed)); +static_assert(sizeof(struct DNS_HEADER) == 12); // Constant sized fields of query structure struct QUESTION @@ -65,15 +74,18 @@ struct QUESTION uint16_t qtype; uint16_t qclass; }; +static_assert(sizeof(struct QUESTION) == 4); // Constant sized fields of the resource record structure struct R_DATA { uint16_t type; uint16_t class; - uint32_t ttl; // RFC 1035 defines it as positive values of a signed 32bit number + uint32_t ttl; // RFC 1035 defines the TTL field as "positive values of a signed 32bit number" uint16_t data_len; } __attribute__((packed)); +static_assert(sizeof(struct R_DATA) == 10); +_Pragma("GCC diagnostic pop") // Pointers to resource record contents struct RES_RECORD @@ -279,13 +291,15 @@ static char *__attribute__((malloc)) ngethostbyname(const char *host, const char static u_char * __attribute__((malloc)) __attribute__((nonnull(1,2,3))) name_fromDNS(unsigned char *reader, unsigned char *buffer, uint16_t *count) { unsigned char *name = calloc(MAXHOSTNAMELEN, sizeof(char)); - unsigned int p = 0, jumped = 0, offset = 0; + unsigned int p = 0, jumped = 0, offset; + // Initialize count *count = 1; // Parse DNS label string encoding (e.g, 3www6google3com) while(*reader!= 0) { + // Check for too long host names if(*reader >= 192) { offset = (*reader)*256 + *(reader + 1) - 49152; // 49152 = 11000000 00000000 From 3b37ca9966f73701d027190d1e95144428896136 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Sat, 16 Dec 2023 21:28:58 +0100 Subject: [PATCH 4/5] Add clarifying comments about the rather obscure DNS message compression feature. DNS should have used LZ77 instead of its own sophomoric compression algorithm. Signed-off-by: DL6ER --- src/resolve.c | 55 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/src/resolve.c b/src/resolve.c index 7b56f397e..d911107ca 100644 --- a/src/resolve.c +++ b/src/resolve.c @@ -287,27 +287,60 @@ static char *__attribute__((malloc)) ngethostbyname(const char *host, const char } // Convert hostname from network to host representation +// This routine supports DNS compression pointers // 3www6google3com -> www.google.com static u_char * __attribute__((malloc)) __attribute__((nonnull(1,2,3))) name_fromDNS(unsigned char *reader, unsigned char *buffer, uint16_t *count) { unsigned char *name = calloc(MAXHOSTNAMELEN, sizeof(char)); - unsigned int p = 0, jumped = 0, offset; + unsigned int p = 0, jumped = 0; // Initialize count *count = 1; // Parse DNS label string encoding (e.g, 3www6google3com) - while(*reader!= 0) + // + // In its text format, a domain name is a sequence of dot-separated + // "labels". The dot separators are not used in binary DNS messages. + // Instead, each label is preceded by a byte containing its length, and + // the name is terminated by a zero-length label representing the root + // zone. + while(*reader != 0) { - // Check for too long host names - if(*reader >= 192) + if(*reader >= 0xC0) { - offset = (*reader)*256 + *(reader + 1) - 49152; // 49152 = 11000000 00000000 + // RFC 1035, Section 4.1.4: Message compression + // + // A label can be up to 63 bytes long; if the length + // byte is 64 (0x40) or greater, it has a special + // meaning. Values between 0x40 and 0xBF have no purpose + // except to cause painful memories for those involved + // in DNS extensions in the late 1990s. + // + // However, if the length byte is 0xC0 or greater, the + // length byte and the next byte form a "compression + // pointer". A DNS name compression pointer allows DNS + // messages to reuse parent domains. The lower 14 bits + // are an offset into the DNS message where the + // remaining suffix of the name previously occurred. + // + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | 1 1| OFFSET | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // + // The first two bits are ones. This allows a pointer + // to be distinguished from a label, since the label + // must begin with two zero bits because labels are + // restricted to 63 octets or less. See the referenced + // RFC for more details. + const unsigned int offset = ((*reader - 0xC0) << 8) + *(reader + 1); reader = buffer + offset - 1; jumped = 1; // We have jumped to another location so counting won't go up } else + // Copy character to name name[p++] = *reader; + + // Increment read pointer reader = reader + 1; if(jumped == 0) @@ -321,17 +354,17 @@ static u_char * __attribute__((malloc)) __attribute__((nonnull(1,2,3))) name_fro if(jumped == 1) *count += 1; - // now convert 3www6google3com0 to www.google.com + // Now convert 3www6google3com0 to www.google.com unsigned int i = 0; for(; i < strlen((const char*)name); i++) { - p=name[i]; + p = name[i]; for(unsigned j = 0; j < p; j++) { - name[i]=name[i+1]; - i=i+1; + name[i] = name[i + 1]; + i = i + 1; } - name[i]='.'; + name[i] = '.'; } // Strip off the trailing dot @@ -341,6 +374,8 @@ static u_char * __attribute__((malloc)) __attribute__((nonnull(1,2,3))) name_fro // Convert hostname from host to network representation // www.google.com -> 3www6google3com +// We do not use DNS compression pointers here as we do not know if the DNS +// server we are talking to supports them static void __attribute__((nonnull(1,3))) name_toDNS(unsigned char *dns, const size_t dnslen, const char *host, const size_t hostlen) { unsigned int lock = 0; From 4cbe8b42c6e9551bee7b880b0f5edd9e840dbddf Mon Sep 17 00:00:00 2001 From: DL6ER Date: Mon, 18 Dec 2023 21:03:14 +0100 Subject: [PATCH 5/5] Fix dns.piholePTR description and add another fallback layer for obtaining the DNS domain used by the system (if not specified through Pi-hole) Signed-off-by: DL6ER --- src/config/config.c | 4 ++-- src/daemon.c | 23 +++++++++++++++++++++-- src/daemon.h | 1 + src/dnsmasq_interface.c | 8 +++++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/config/config.c b/src/config/config.c index d33da50e1..c202445fa 100644 --- a/src/config/config.c +++ b/src/config/config.c @@ -429,8 +429,8 @@ void initConfig(struct config *conf) struct enum_options piholePTR[] = { { get_ptr_type_str(PTR_NONE), "Pi-hole will not respond automatically on PTR requests to local interface addresses. Ensure pi.hole and/or hostname records exist elsewhere." }, - { get_ptr_type_str(PTR_HOSTNAME), "Pi-hole will not respond automatically on PTR requests to local interface addresses. Ensure pi.hole and/or hostname records exist elsewhere." }, - { get_ptr_type_str(PTR_HOSTNAMEFQDN), "Serve the machine's global hostname as fully qualified domain by adding the local suffix. If no local suffix has been defined, FTL appends the local domain .no_fqdn_available. In this case you should either add domain=whatever.com to a custom config file inside /etc/dnsmasq.d/ (to set whatever.com as local domain) or use domain=# which will try to derive the local domain from /etc/resolv.conf (or whatever is set with resolv-file, when multiple search directives exist, the first one is used)." }, + { get_ptr_type_str(PTR_HOSTNAME), "Serve the machine's hostname. The hostname is queried from the kernel through uname(2)->nodename. If the machine has multiple network interfaces, it can also have multiple nodenames. In this case, it is unspecified and up to the kernel which one will be returned. On Linux, the returned string is what has been set using sethostname(2) which is typically what has been set in /etc/hostname." }, + { get_ptr_type_str(PTR_HOSTNAMEFQDN), "Serve the machine's hostname (see limitations above) as fully qualified domain by adding the local domain. If no local domain has been defined (config option dns.domain), FTL tries to query the domain name from the kernel using getdomainname(2). If this fails, FTL appends \".no_fqdn_available\" to the hostname." }, { get_ptr_type_str(PTR_PIHOLE), "Respond with \"pi.hole\"." } }; CONFIG_ADD_ENUM_OPTIONS(conf->dns.piholePTR.a, piholePTR); diff --git a/src/daemon.c b/src/daemon.c index b4d07d23a..510c06af2 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -178,20 +178,39 @@ char *getUserName(void) // hyphen. #define HOSTNAMESIZE 256 static char nodename[HOSTNAMESIZE] = { 0 }; +static char dname[HOSTNAMESIZE] = { 0 }; + +// Returns the hostname of the system const char *hostname(void) { // Ask kernel for node name if not known // This is equivalent to "uname -n" + // + // According to man gethostname(2), this is exactly the same as calling + // getdomainname() just with one step less if(nodename[0] == '\0') { struct utsname buf; if(uname(&buf) == 0) + { strncpy(nodename, buf.nodename, HOSTNAMESIZE); - nodename[HOSTNAMESIZE-1] = '\0'; + strncpy(dname, buf.domainname, HOSTNAMESIZE); + } + nodename[HOSTNAMESIZE - 1] = '\0'; + dname[HOSTNAMESIZE - 1] = '\0'; } return nodename; } +// Returns the domain name of the system +const char *domainname(void) +{ + if(dname[0] == '\0') + hostname(); + + return dname; +} + void delay_startup(void) { // Exit early if not sleeping @@ -463,4 +482,4 @@ void init_locale(void) // the dot as decimal separator (even if the system locale uses a // comma, e.g., in German) setlocale(LC_NUMERIC, "C"); -} \ No newline at end of file +} diff --git a/src/daemon.h b/src/daemon.h index bd86cb147..f66747bf7 100644 --- a/src/daemon.h +++ b/src/daemon.h @@ -17,6 +17,7 @@ void go_daemon(void); void savepid(void); char *getUserName(void); const char *hostname(void); +const char *domainname(void); void delay_startup(void); bool is_fork(const pid_t mpid, const pid_t pid) __attribute__ ((const)); void cleanup(const int ret); diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index 1e1ab3b1c..1459e5cc7 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -2935,10 +2935,16 @@ static char *get_ptrname(struct in_addr *addr) suffix = get_domain(*addr); else suffix = 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 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) + if(!suffix || suffix[0] == '\0') suffix = (char*)"no_fqdn_available"; // Get enough space for domain building