diff --git a/CMakeLists.txt b/CMakeLists.txt index ee816d6c4..d261b630a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,6 @@ set(CMAKE_C_STANDARD 17) project(PIHOLE_FTL C) -set(DNSMASQ_VERSION pi-hole-v2.91test8) +set(DNSMASQ_VERSION pi-hole-v2.91test9) add_subdirectory(src) diff --git a/src/database/query-table.c b/src/database/query-table.c index 1231ce238..0b07d52d7 100644 --- a/src/database/query-table.c +++ b/src/database/query-table.c @@ -1122,7 +1122,7 @@ void DB_read_queries(void) } const int type = sqlite3_column_int(stmt, 2); - const bool mapped_type = type >= TYPE_A && type < TYPE_MAX; + const bool mapped_type = type >= TYPE_NONE && type < TYPE_MAX; const bool offset_type = type > 100 && type < (100 + UINT16_MAX); if(!mapped_type && !offset_type) { diff --git a/src/datastructure.c b/src/datastructure.c index 1ad2511a0..b78f4663c 100644 --- a/src/datastructure.c +++ b/src/datastructure.c @@ -690,6 +690,8 @@ const char *get_query_type_str(const enum query_type type, const queriesData *qu { switch (type) { + case TYPE_NONE: + return "NONE"; case TYPE_A: return "A"; case TYPE_AAAA: diff --git a/src/dnsmasq/auth.c b/src/dnsmasq/auth.c index efe838555..cd4fe44da 100644 --- a/src/dnsmasq/auth.c +++ b/src/dnsmasq/auth.c @@ -137,7 +137,7 @@ size_t answer_auth(struct dns_header *header, char *limit, size_t qlen, time_t n nameoffset = p - (unsigned char *)header; /* now extract name as .-concatenated string into name */ - if (!extract_name(header, qlen, &p, name, 1, 4)) + if (!extract_name(header, qlen, &p, name, EXTR_NAME_EXTRACT, 4)) return 0; /* bad packet */ GETSHORT(qtype, p); diff --git a/src/dnsmasq/cache.c b/src/dnsmasq/cache.c index 35a6d10ea..ae806fdc8 100644 --- a/src/dnsmasq/cache.c +++ b/src/dnsmasq/cache.c @@ -1886,15 +1886,31 @@ int cache_make_stat(struct txt_record *t) #endif /* There can be names in the cache containing control chars, don't - mess up logging or open security holes. */ + mess up logging or open security holes. Also convert to all-LC + so that 0x20-encoding doesn't make logs look like ransom notes + made out of letters cut from a newspaper. + Overwrites daemon->workspacename */ static char *sanitise(char *name) { - unsigned char *r; + unsigned char *r = (unsigned char *)name; + if (name) - for (r = (unsigned char *)name; *r; r++) - if (!isprint((int)*r)) - return ""; - + { + char *d = name = daemon->workspacename; + + for (; *r; r++, d++) + if (!isprint((int)*r)) + return ""; + else + { + unsigned char c = *r; + + *d = (char)((c >= 'A' && c <= 'Z') ? c + 'a' - 'A' : c); + } + + *d = 0; + } + return name; } diff --git a/src/dnsmasq/dnsmasq.h b/src/dnsmasq/dnsmasq.h index d46313550..338342f81 100644 --- a/src/dnsmasq/dnsmasq.h +++ b/src/dnsmasq/dnsmasq.h @@ -804,6 +804,7 @@ struct frec { int forwardall, flags; time_t time; u32 forward_timestamp; + unsigned int encode_bitmap; int forward_delay; struct blockdata *stash; /* saved query or saved reply, whilst we validate */ size_t stash_len; @@ -1404,7 +1405,7 @@ int is_rev_synth(int flag, union all_addr *addr, char *name); /* rfc1035.c */ int do_doctor(struct dns_header *header, size_t qlen, char *namebuff); int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, - char *name, int isExtract, int extrabytes); + char *name, int func, unsigned int parm); unsigned char *skip_name(unsigned char *ansp, struct dns_header *header, size_t plen, int extrabytes); unsigned char *skip_questions(struct dns_header *header, size_t plen); unsigned char *skip_section(unsigned char *ansp, int count, struct dns_header *header, size_t plen); @@ -1431,6 +1432,11 @@ int add_resource_record(struct dns_header *header, char *limit, int *truncp, int *offset, unsigned short type, unsigned short class, char *format, ...); int in_arpa_name_2_addr(char *namein, union all_addr *addrp); int private_net(struct in_addr addr, int ban_localhost); +/* extract_name ops */ +#define EXTR_NAME_EXTRACT 1 +#define EXTR_NAME_COMPARE 2 +#define EXTR_NAME_NOCASE 3 +#define EXTR_NAME_FLIP 4 /* auth.c */ #ifdef HAVE_AUTH diff --git a/src/dnsmasq/dnssec.c b/src/dnsmasq/dnssec.c index a3ca1ab1c..31a47e4ff 100644 --- a/src/dnsmasq/dnssec.c +++ b/src/dnsmasq/dnssec.c @@ -192,7 +192,7 @@ static int get_rdata(struct dns_header *header, size_t plen, struct rdata_state /* domain-name, canonicalise */ int len; - if (!extract_name(header, plen, &state->ip, state->buff, 1, 0) || + if (!extract_name(header, plen, &state->ip, state->buff, EXTR_NAME_EXTRACT, 0) || (len = to_wire(state->buff)) == 0) continue; @@ -340,7 +340,7 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int pstart = p; - if (!(res = extract_name(header, plen, &p, name, 0, 10))) + if (!(res = extract_name(header, plen, &p, name, EXTR_NAME_COMPARE, 10))) return 0; /* bad packet */ GETSHORT(stype, p); @@ -375,14 +375,14 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int if (gotkey) { /* If there's more than one SIG, ensure they all have same keyname */ - if (extract_name(header, plen, &p, keyname, 0, 0) != 1) + if (extract_name(header, plen, &p, keyname, EXTR_NAME_COMPARE, 0) != 1) return 0; } else { gotkey = 1; - if (!extract_name(header, plen, &p, keyname, 1, 0)) + if (!extract_name(header, plen, &p, keyname, EXTR_NAME_EXTRACT, 0)) return 0; /* RFC 4035 5.3.1 says that the Signer's Name field MUST equal @@ -504,7 +504,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in GETLONG(sig_inception, p); GETSHORT(key_tag, p); - if (!extract_name(header, plen, &p, keyname, 1, 0)) + if (!extract_name(header, plen, &p, keyname, EXTR_NAME_EXTRACT, 0)) return STAT_BOGUS; if (!time_check) @@ -575,7 +575,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in p = rrset[i]; - if (!extract_name(header, plen, &p, name, 1, 10)) + if (!extract_name(header, plen, &p, name, EXTR_NAME_EXTRACT, 10)) return STAT_BOGUS; name_start = name; @@ -668,7 +668,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in /* namebuff used for workspace above, restore to leave unchanged on exit */ p = (unsigned char*)(rrset[0]); - if (!extract_name(header, plen, &p, name, 1, 0)) + if (!extract_name(header, plen, &p, name, EXTR_NAME_EXTRACT, 0)) return STAT_BOGUS; if (key) @@ -734,7 +734,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch static unsigned char **cached_digest; static size_t cached_digest_size = 0; - if (ntohs(header->qdcount) != 1 || RCODE(header) != NOERROR || !extract_name(header, plen, &p, name, 1, 4)) + if (ntohs(header->qdcount) != 1 || RCODE(header) != NOERROR || !extract_name(header, plen, &p, name, EXTR_NAME_EXTRACT, 4)) return STAT_BOGUS | DNSSEC_FAIL_NOKEY; GETSHORT(qtype, p); @@ -759,7 +759,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch for (j = ntohs(header->ancount); j != 0; j--) { /* Ensure we have type, class TTL and length */ - if (!(rc = extract_name(header, plen, &p, name, 0, 10))) + if (!(rc = extract_name(header, plen, &p, name, EXTR_NAME_COMPARE, 10))) return STAT_BOGUS; /* bad packet */ GETSHORT(qtype, p); @@ -911,7 +911,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch for (j = ntohs(header->ancount); j != 0; j--) { /* Ensure we have type, class TTL and length */ - if (!(rc = extract_name(header, plen, &p, name, 0, 10))) + if (!(rc = extract_name(header, plen, &p, name, EXTR_NAME_COMPARE, 10))) return STAT_BOGUS; /* bad packet */ GETSHORT(qtype, p); @@ -1031,7 +1031,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char } p = (unsigned char *)(header+1); - if (!extract_name(header, plen, &p, name, 1, 4)) + if (!extract_name(header, plen, &p, name, EXTR_NAME_EXTRACT, 4)) return STAT_BOGUS; p += 4; /* qtype, qclass */ @@ -1057,7 +1057,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char { unsigned char *psave; - if (!(rc = extract_name(header, plen, &p, name, 0, 10))) + if (!(rc = extract_name(header, plen, &p, name, EXTR_NAME_COMPARE, 10))) return STAT_BOGUS; /* bad packet */ GETSHORT(atype, p); @@ -1238,12 +1238,12 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi int sig_labels, name_labels; p = nsecs[i]; - if (!extract_name(header, plen, &p, workspace1, 1, 10)) + if (!extract_name(header, plen, &p, workspace1, EXTR_NAME_EXTRACT, 10)) return DNSSEC_FAIL_BADPACKET; p += 8; /* class, type, TTL */ GETSHORT(rdlen, p); psave = p; - if (!extract_name(header, plen, &p, workspace2, 1, 0)) + if (!extract_name(header, plen, &p, workspace2, EXTR_NAME_EXTRACT, 0)) return DNSSEC_FAIL_BADPACKET; /* If NSEC comes from wildcard expansion, use original wildcard @@ -1407,7 +1407,7 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige for (i = 0; i < nsec_count; i++) if ((p = nsecs[i])) { - if (!extract_name(header, plen, &p, workspace1, 1, 10) || + if (!extract_name(header, plen, &p, workspace1, EXTR_NAME_EXTRACT, 10) || !(base32_len = base32_decode(workspace1, (unsigned char *)workspace2))) return 0; @@ -1616,7 +1616,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns for (i = 0; i < nsec_count; i++) if ((p = nsecs[i])) { - if (!extract_name(header, plen, &p, workspace1, 1, 0)) + if (!extract_name(header, plen, &p, workspace1, EXTR_NAME_EXTRACT, 0)) return DNSSEC_FAIL_BADPACKET; if (!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2))) @@ -1691,7 +1691,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key { unsigned char *pstart = p; - if (!extract_name(header, plen, &p, daemon->workspacename, 1, 10)) + if (!extract_name(header, plen, &p, daemon->workspacename, EXTR_NAME_EXTRACT, 10)) return DNSSEC_FAIL_BADPACKET; GETSHORT(type, p); @@ -1742,7 +1742,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key { unsigned char *psav; - if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10))) + if (!(res = extract_name(header, plen, &p1, daemon->workspacename, EXTR_NAME_COMPARE, 10))) return DNSSEC_FAIL_BADPACKET; GETSHORT(type1, p1); @@ -1972,7 +1972,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch targets[0] = p1; targetidx = 1; - if (!extract_name(header, plen, &p1, name, 1, 4)) + if (!extract_name(header, plen, &p1, name, EXTR_NAME_EXTRACT, 4)) return STAT_BOGUS; GETSHORT(qtype, p1); @@ -2010,7 +2010,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch if (i != 0 && !ADD_RDLEN(header, p1, plen, rdlen1)) return STAT_BOGUS; - if (!extract_name(header, plen, &p1, name, 1, 10)) + if (!extract_name(header, plen, &p1, name, EXTR_NAME_EXTRACT, 10)) return STAT_BOGUS; /* bad packet */ GETSHORT(type1, p1); @@ -2025,7 +2025,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch /* Check if we've done this RRset already */ for (p2 = ans_start, j = 0; j < i; j++) { - if (!(rc = extract_name(header, plen, &p2, name, 0, 10))) + if (!(rc = extract_name(header, plen, &p2, name, EXTR_NAME_COMPARE, 10))) return STAT_BOGUS; /* bad packet */ GETSHORT(type2, p2); @@ -2122,7 +2122,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch if ((p2 = targets[j])) { int rc1; - if (!(rc1 = extract_name(header, plen, &p2, name, 0, 10))) + if (!(rc1 = extract_name(header, plen, &p2, name, EXTR_NAME_COMPARE, 10))) return STAT_BOGUS; /* bad packet */ if (class1 == qclass && rc1 == 1 && (type1 == T_CNAME || type1 == qtype || qtype == T_ANY )) @@ -2156,7 +2156,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch if (neganswer) *neganswer = 1; - if (!extract_name(header, plen, &p2, name, 1, 10)) + if (!extract_name(header, plen, &p2, name, EXTR_NAME_EXTRACT, 10)) return STAT_BOGUS; /* bad packet */ /* NXDOMAIN or NODATA reply, unanswered question is (name, qclass, qtype) */ diff --git a/src/dnsmasq/forward.c b/src/dnsmasq/forward.c index a0d4842d1..7c26c7489 100644 --- a/src/dnsmasq/forward.c +++ b/src/dnsmasq/forward.c @@ -275,6 +275,8 @@ static void forward_query(int udpfd, union mysockaddr *udpaddr, /* new query */ if (!forward) { + unsigned char *p; + if (OPCODE(header) != QUERY) { flags = F_RCODE; @@ -334,7 +336,12 @@ static void forward_query(int udpfd, union mysockaddr *udpaddr, forward->frec_src.orig_id = ntohs(header->id); forward->new_id = get_id(); header->id = ntohs(forward->new_id); - + + forward->encode_bitmap = rand32(); + p = (unsigned char *)(header+1); + if (!extract_name(header, plen, &p, NULL, EXTR_NAME_FLIP, forward->encode_bitmap)) + goto reply; + /* Keep copy of query for retries and move to TCP */ if (!(forward->stash = blockdata_alloc((char *)header, plen))) { @@ -1030,7 +1037,8 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header, new->flags &= ~(FREC_DNSKEY_QUERY | FREC_DS_QUERY); new->flags |= flags; new->forwardall = 0; - + new->encode_bitmap = 0; + forward->next_dependent = NULL; new->dependent = forward; /* to find query awaiting new one. */ @@ -1147,7 +1155,7 @@ void reply_query(int fd, time_t now) return; p = (unsigned char *)(header+1); - if (!extract_name(header, n, &p, daemon->namebuff, 1, 4)) + if (!extract_name(header, n, &p, daemon->namebuff, EXTR_NAME_EXTRACT, 4)) return; /* bad packet */ GETSHORT(rrtype, p); GETSHORT(class, p); @@ -1259,7 +1267,11 @@ void reply_query(int fd, time_t now) /* denominator controls how many queries we average over. */ server->query_latency = server->mma_latency/128; - + /* Flip the bits back in the query name. */ + p = (unsigned char *)(header+1); + if (!extract_name(header, n, &p, NULL, EXTR_NAME_FLIP, forward->encode_bitmap)) + return; + #ifdef HAVE_DNSSEC if (option_bool(OPT_DNSSEC_VALID)) { @@ -1762,7 +1774,11 @@ void receive_query(struct listener *listen, time_t now) //******************************************************************// if (OPCODE(header) != QUERY) - log_query_mysockaddr(F_QUERY | F_FORWARD, "opcode", &source_addr, "non-query", 0); + { + log_query_mysockaddr(F_QUERY | F_FORWARD, "opcode", &source_addr, "non-query", 0); + piholeblocked = FTL_new_query(F_QUERY | F_FORWARD , "opcode", + &source_addr, "non-query", 0, daemon->log_display_id, UDP); + } else if (extract_request(header, (size_t)n, daemon->namebuff, &type)) { #ifdef HAVE_AUTH @@ -2007,7 +2023,7 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, /* Save the query to make sure we get the answer we expect. */ p = (unsigned char *)(header+1); - if (!extract_name(header, qsize, &p, daemon->namebuff, 1, 4)) + if (!extract_name(header, qsize, &p, daemon->namebuff, EXTR_NAME_EXTRACT, 4)) return 0; GETSHORT(type, p); GETSHORT(class, p); @@ -2122,12 +2138,12 @@ static ssize_t tcp_talk(int first, int last, int start, unsigned char *packet, continue; } - /* If the question section of the reply doesn't match the crc we sent, then + /* If the question section of the reply doesn't match the question we sent, then someone might be attempting to insert bogus values into the cache by - sending replies containing questions and bogus answers. + sending replies containing questions and bogus answers. Try another server, or give up */ p = (unsigned char *)(header+1); - if (extract_name(header, rsize, &p, daemon->namebuff, 0, 4) != 1) + if (extract_name(header, rsize, &p, daemon->namebuff, EXTR_NAME_NOCASE, 4) != 1) continue; GETSHORT(rtype, p); GETSHORT(rclass, p); @@ -2455,6 +2471,8 @@ unsigned char *tcp_request(int confd, time_t now, log_query_mysockaddr(F_QUERY | F_FORWARD, "opcode", &peer_addr, "non-query", 0); gotname = 0; flags = F_RCODE; + piholeblocked = FTL_new_query(F_QUERY | F_FORWARD , "opcode", + &peer_addr, "non-query", 0, daemon->log_display_id, TCP); } else if (!(gotname = extract_request(header, (unsigned int)size, daemon->namebuff, &qtype))) ede = EDE_INVALID_DATA; @@ -3204,8 +3222,9 @@ static struct frec *lookup_frec(char *target, int class, int rrtype, int id, int { unsigned char *p = (unsigned char *)(header+1); int hclass, hrrtype; - - if (extract_name(header, f->stash_len, &p, target, 0, 4) != 1) + + /* Case sensitive compare for DNS-0x20 encoding. */ + if (extract_name(header, f->stash_len, &p, target, EXTR_NAME_NOCASE, 4) != 1) continue; GETSHORT(hrrtype, p); diff --git a/src/dnsmasq/option.c b/src/dnsmasq/option.c index 2634af32d..03caec5a2 100644 --- a/src/dnsmasq/option.c +++ b/src/dnsmasq/option.c @@ -572,7 +572,7 @@ static struct { { LOPT_CMARK_ALST, ARG_DUP, "[/][,[/...]]", gettext_noop("Set allowed DNS patterns for a connection-track mark."), NULL }, { LOPT_SYNTH, ARG_DUP, ",,[]", gettext_noop("Specify a domain and address range for synthesised names"), NULL }, { LOPT_SEC_VALID, OPT_DNSSEC_VALID, NULL, gettext_noop("Activate DNSSEC validation"), NULL }, - { LOPT_TRUST_ANCHOR, ARG_DUP, ",[],...", gettext_noop("Specify trust anchor key digest."), NULL }, + { LOPT_TRUST_ANCHOR, ARG_DUP, ",[,]...", gettext_noop("Specify trust anchor key digest."), NULL }, { LOPT_DNSSEC_DEBUG, OPT_DNSSEC_DEBUG, NULL, gettext_noop("Disable upstream checking for DNSSEC debugging."), NULL }, { LOPT_DNSSEC_CHECK, ARG_DUP, NULL, gettext_noop("Ensure answers without DNSSEC are in unsigned zones."), NULL }, { LOPT_DNSSEC_TIME, OPT_DNSSEC_TIME, NULL, gettext_noop("Don't check DNSSEC signature timestamps until first cache-reload"), NULL }, diff --git a/src/dnsmasq/rfc1035.c b/src/dnsmasq/rfc1035.c index 59d644fc3..b46b232ef 100644 --- a/src/dnsmasq/rfc1035.c +++ b/src/dnsmasq/rfc1035.c @@ -17,16 +17,29 @@ #include "dnsmasq.h" #include "dnsmasq_interface.h" +/* EXTR_NAME_EXTRACT -> extract name + EXTR_NAME_COMPARE -> compare name, case insensitive + EXTR_NAME_NOCASE -> compare name, case sensitive + EXTR_NAME_FLIP -> flip 0x20 bits in packet, controlled by bitmap in parm. name may be NULL + + return = 0 -> error + return = 1 -> extract OK, compare OK, flip OK + return = 2 -> extract OK, compare failed. +*/ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, - char *name, int isExtract, int extrabytes) + char *name, int func, unsigned int parm) { unsigned char *cp = (unsigned char *)name, *p = *pp, *p1 = NULL; unsigned int j, l, namelen = 0, hops = 0; - int retvalue = 1; + int retvalue = 1, case_insens = 1, isExtract = 0, flip = 0, extrabytes = (int)parm; + + if (func == EXTR_NAME_EXTRACT) + isExtract = 1, *cp = 0; + else if (func == EXTR_NAME_NOCASE) + case_insens = 0; + else if (func == EXTR_NAME_FLIP) + flip = 1, extrabytes = 0; - if (isExtract) - *cp = 0; - while (1) { unsigned int label_type; @@ -47,7 +60,7 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, cp--; *cp = 0; /* terminate: lose final period */ } - else if (*cp != 0) + else if (!flip && *cp != 0) retvalue = 2; if (p1) /* we jumped via compression */ @@ -86,7 +99,7 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, if (!CHECK_LEN(header, p, plen, l)) return 0; - for(j=0; j= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + { + if (parm & 1) + *p ^= 0x20; + parm >>= 1; + } + } else { unsigned char c1 = *cp, c2 = *p; @@ -108,15 +133,15 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, else { cp++; - if (c1 >= 'A' && c1 <= 'Z') - c1 += 'a' - 'A'; if (c1 == NAME_ESCAPE) c1 = (*cp++)-1; + else if (case_insens && c1 >= 'A' && c1 <= 'Z') + c1 += 'a' - 'A'; - if (c2 >= 'A' && c2 <= 'Z') + if (case_insens && c2 >= 'A' && c2 <= 'Z') c2 += 'a' - 'A'; - + if (c1 != c2) retvalue = 2; } @@ -124,7 +149,7 @@ int extract_name(struct dns_header *header, size_t plen, unsigned char **pp, if (isExtract) *cp++ = '.'; - else if (*cp != 0 && *cp++ != '.') + else if (!flip && *cp != 0 && *cp++ != '.') retvalue = 2; } else @@ -400,7 +425,7 @@ int do_doctor(struct dns_header *header, size_t qlen, char *namebuff) if (i == ntohs(header->ancount) && !(p = skip_section(p, ntohs(header->nscount), header, qlen))) return done; - if (!extract_name(header, qlen, &p, namebuff, 1, 10)) + if (!extract_name(header, qlen, &p, namebuff, EXTR_NAME_EXTRACT, 10)) return done; /* bad packet */ GETSHORT(qtype, p); @@ -481,7 +506,7 @@ static int find_soa(struct dns_header *header, size_t qlen, char *name, int *sub for (i = 0; i < ntohs(header->nscount); i++) { - if (!extract_name(header, qlen, &p, daemon->workspacename, 1, 0)) + if (!extract_name(header, qlen, &p, daemon->workspacename, EXTR_NAME_EXTRACT, 0)) return 0; /* bad packet */ GETSHORT(qtype, p); @@ -510,7 +535,7 @@ static int find_soa(struct dns_header *header, size_t qlen, char *name, int *sub for (j = 0; j < 2; j++) /* MNAME, RNAME */ { - if (!extract_name(header, qlen, &p, daemon->workspacename, 1, 0)) + if (!extract_name(header, qlen, &p, daemon->workspacename, EXTR_NAME_EXTRACT, 0)) { if (!no_cache) blockdata_free(addr.rrblock.rrdata); @@ -662,7 +687,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t namep = p = (unsigned char *)(header+1); - if (ntohs(header->qdcount) != 1 || !extract_name(header, qlen, &p, name, 1, 4)) + if (ntohs(header->qdcount) != 1 || !extract_name(header, qlen, &p, name, EXTR_NAME_EXTRACT, 4)) return 2; /* bad packet */ GETSHORT(qtype, p); @@ -686,7 +711,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t for (j = 0; j < ntohs(header->ancount); j++) { int secflag = 0; - if (!(res = extract_name(header, qlen, &p1, name, 0, 10))) + if (!(res = extract_name(header, qlen, &p1, name, EXTR_NAME_COMPARE, 10))) return 2; /* bad packet */ GETSHORT(aqtype, p1); @@ -723,7 +748,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t if (aqtype == T_CNAME) log_query(secflag | F_CNAME | F_FORWARD | F_UPSTREAM, name, NULL, NULL, 0); - if (!extract_name(header, qlen, &p1, name, 1, 0)) + if (!extract_name(header, qlen, &p1, name, EXTR_NAME_EXTRACT, 0)) return 2; if (aqtype == T_CNAME) @@ -812,7 +837,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t { int secflag = 0; - if (!(res = extract_name(header, qlen, &p1, name, 0, 10))) + if (!(res = extract_name(header, qlen, &p1, name, EXTR_NAME_COMPARE, 10))) return 2; /* bad packet */ GETSHORT(aqtype, p1); @@ -875,7 +900,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t } namep = p1; - if (!extract_name(header, qlen, &p1, name, 1, 0)) + if (!extract_name(header, qlen, &p1, name, EXTR_NAME_EXTRACT, 0)) return 2; // ****************************** Pi-hole modification ****************************** @@ -956,7 +981,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t /* Name, extract it then re-encode. */ int len; - if (!extract_name(header, qlen, &p1, name, 1, 0)) + if (!extract_name(header, qlen, &p1, name, EXTR_NAME_EXTRACT, 0)) { blockdata_free(addr.rrblock.rrdata); return 2; @@ -989,7 +1014,7 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t } while (desc != -1); /* we overwrote the original name, so get it back here. */ - if (!extract_name(header, qlen, &tmp, name, 1, 0)) + if (!extract_name(header, qlen, &tmp, name, EXTR_NAME_EXTRACT, 0)) { blockdata_free(addr.rrblock.rrdata); return 2; @@ -1160,7 +1185,7 @@ void report_addresses(struct dns_header *header, size_t len, u32 mark) { int aqtype, aqclass, ardlen; - if (!extract_name(header, len, &p, daemon->namebuff, 1, 10)) + if (!extract_name(header, len, &p, daemon->namebuff, EXTR_NAME_EXTRACT, 10)) return; if (!CHECK_LEN(header, p, len, 10)) @@ -1178,7 +1203,7 @@ void report_addresses(struct dns_header *header, size_t len, u32 mark) { if (aqtype == T_CNAME) { - if (!extract_name(header, len, &p, daemon->workspacename, 1, 0)) + if (!extract_name(header, len, &p, daemon->workspacename, EXTR_NAME_EXTRACT, 0)) return; if (safe_name(daemon->namebuff) && safe_name(daemon->workspacename)) ubus_event_bcast_connmark_allowlist_resolved(mark, daemon->namebuff, daemon->workspacename, attl); @@ -1228,7 +1253,7 @@ unsigned int extract_request(struct dns_header *header, size_t qlen, char *name, if (!(header->hb3 & HB3_QR) && (ntohs(header->ancount) != 0 || ntohs(header->nscount) != 0)) return 0; /* non-standard query. */ - if (!extract_name(header, qlen, &p, name, 1, 4)) + if (!extract_name(header, qlen, &p, name, EXTR_NAME_EXTRACT, 4)) return 0; /* bad packet */ GETSHORT(qtype, p); @@ -1342,7 +1367,7 @@ static int check_bad_address(struct dns_header *header, size_t qlen, struct bogu for (i = ntohs(header->ancount); i != 0; i--) { - if (name && !extract_name(header, qlen, &p, name, 1, 10)) + if (name && !extract_name(header, qlen, &p, name, EXTR_NAME_EXTRACT, 10)) return 0; /* bad packet */ if (!name && !(p = skip_name(p, header, qlen, 10))) @@ -1652,7 +1677,7 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, nameoffset = p - (unsigned char *)header; /* now extract name as .-concatenated string into name */ - if (!extract_name(header, qlen, &p, name, 1, 4)) + if (!extract_name(header, qlen, &p, name, EXTR_NAME_EXTRACT, 4)) return 0; /* bad packet */ GETSHORT(qtype, p); diff --git a/src/dnsmasq_interface.c b/src/dnsmasq_interface.c index c181065ff..aa594884f 100644 --- a/src/dnsmasq_interface.c +++ b/src/dnsmasq_interface.c @@ -77,6 +77,7 @@ static void check_pihole_PTR(char *domain); 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; @@ -601,6 +602,14 @@ bool _FTL_new_query(const unsigned int flags, const char *name, enum query_type querytype; switch(qtype) { + case 0: + // Non-query, e.g., zone update + // dnsmasq does not support such non-queries. RFC5625 + // does not specify how a resolver should behave when it + // does not support them. dnsmasq decided to reply with + // a NOTIMP reply to such non-queries + querytype = TYPE_NONE; + break; case T_A: querytype = TYPE_A; break; @@ -656,7 +665,7 @@ bool _FTL_new_query(const unsigned int flags, const char *name, // 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(is_pihole_domain(name)) + if(querytype != TYPE_NONE && is_pihole_domain(name)) { if(querytype == TYPE_A || querytype == TYPE_AAAA || querytype == TYPE_ANY) { @@ -811,9 +820,9 @@ bool _FTL_new_query(const unsigned int flags, const char *name, if(config.debug.queries.v.b) { const char *types = querystr(arg, qtype); - log_debug(DEBUG_QUERIES, "**** new %sIPv%d %s query \"%s\" from %s/%s#%d (ID %i, FTL %i, %s:%i)", - proto == TCP ? "TCP " : proto == UDP ? "UDP " : "", - family == AF_INET ? 4 : 6, types, domainString, interface, + 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); } @@ -827,10 +836,8 @@ bool _FTL_new_query(const unsigned int flags, const char *name, { // Don't process this query further here, we already counted it if(config.debug.queries.v.b) - { - const char *types = querystr(arg, qtype); - log_debug(DEBUG_QUERIES, "Skipping new query: %s (%i)", types, id); - } + log_debug(DEBUG_QUERIES, "Skipping new query (%i)", id); + free(domainString); unlock_shm(); return false; @@ -979,7 +986,7 @@ bool _FTL_new_query(const unsigned int flags, const char *name, bool blockDomain = false; // Check if this should be blocked only for active queries // (skipped for internally generated ones, e.g., DNSSEC) - if(!internal_query) + if(!internal_query && querytype != TYPE_NONE) blockDomain = FTL_check_blocking(queryID, domainID, clientID); // Free allocated memory @@ -2137,9 +2144,9 @@ static void FTL_reply(const unsigned int flags, const char *name, const union al return; } - // Check if this reply came from our local cache + // Check if this reply came from our local cache (type == 0 is non-query but has F_UPSTREAM) bool cached = false; - if(!(flags & F_UPSTREAM)) + if(!(flags & F_UPSTREAM) || type == 0) { cached = true; if((flags & F_HOSTS) || // hostname.list, /etc/hosts and others @@ -2225,7 +2232,7 @@ static void FTL_reply(const unsigned int flags, const char *name, const union al } else { - char ip[ADDRSTRLEN+1] = { 0 }; + 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 @@ -2233,6 +2240,16 @@ static void FTL_reply(const unsigned int flags, const char *name, const union al 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); + } } // Get and check query pointer @@ -2343,9 +2360,10 @@ static void FTL_reply(const unsigned int flags, const char *name, const union al // Mark query for updating in the database query->flags.database.changed = true; } - else if((flags & (F_FORWARD | F_UPSTREAM)) && isExactMatch) + else if((flags & (F_FORWARD | F_UPSTREAM)) && isExactMatch && type != 0) { - // Answered from upstream server + // 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 @@ -2443,6 +2461,21 @@ static void FTL_reply(const unsigned int flags, const char *name, const union al // 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"); @@ -2673,6 +2706,30 @@ static void FTL_dnssec(const char *arg, const union all_addr *addr, const int id 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 @@ -2712,26 +2769,8 @@ static void FTL_upstream_error(const union all_addr *addr, const unsigned int fl // Translate dnsmasq's rcode into something we can use const char *rcodestr = NULL; - enum reply_type reply; - switch(addr->log.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; - } + enum reply_type reply = REPLY_UNKNOWN; + get_rcode(addr->log.rcode, &rcodestr, &reply); // Get EDNS data (if available) ednsData *edns = getEDNS(); @@ -3055,6 +3094,16 @@ static void _query_set_reply(const unsigned int flags, const enum reply_type rep // 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) { diff --git a/src/enums.h b/src/enums.h index 6a81e2358..7216060d2 100644 --- a/src/enums.h +++ b/src/enums.h @@ -98,6 +98,7 @@ enum regex_type { } __attribute__ ((packed)); enum query_type { + TYPE_NONE = 0, TYPE_A = 1, TYPE_AAAA, TYPE_ANY, diff --git a/test/run.sh b/test/run.sh index 598ecff3a..100b82a94 100755 --- a/test/run.sh +++ b/test/run.sh @@ -109,6 +109,9 @@ echo "FTL verbose version (CLI): " echo -n "Contained dnsmasq version (DNS): " dig TXT CHAOS version.bind @127.0.0.1 +short +# Install py3-dnspython +apk add --no-cache py3-dnspython + # Run tests $BATS -p "test/test_suite.bats" RET=$? diff --git a/test/test_suite.bats b/test/test_suite.bats index f98f12b52..6dcc79981 100644 --- a/test/test_suite.bats +++ b/test/test_suite.bats @@ -1524,6 +1524,56 @@ [[ "${lines[0]}" == "1.1.1.1" ]] } +@test "Zone update (non-query) is rejected with NOTIMP (UDP)" { + # Get number of lines in the log before the test + before="$(grep -c ^ /var/log/pihole/FTL.log)" + + # Run test command + run bash -c "python3 test/zone_update.py udp" + printf "%s\n" "${lines[@]}" + [[ "${lines[0]}" == "UDP response: NOTIMP" ]] + [[ "${lines[1]}" == "" ]] + + # Get number of lines in the log after the test + after="$(grep -c ^ /var/log/pihole/FTL.log)" + + # Extract relevant log lines + run bash -c "sed -n \"${before},${after}p\" /var/log/pihole/FTL.log" + + # Check for expected log lines + printf "%s\n" "${lines[@]}" + [[ "${lines[@]}" == *"new UDP IPv4 non-query[type=0] \"opcode\" from lo/127.0.0.1"* ]] + [[ "${lines[@]}" == *"**** got cache reply: opcode is (null) "* ]] +} + +@test "Zone update (non-query) is rejected with NOTIMP (TCP)" { + # Get number of lines in the log before the test + before="$(grep -c ^ /var/log/pihole/FTL.log)" + + # Run test command + run bash -c "python3 test/zone_update.py tcp" + printf "%s\n" "${lines[@]}" + [[ "${lines[0]}" == "TCP response: NOTIMP" ]] + [[ "${lines[1]}" == "" ]] + + # Get number of lines in the log after the test + after="$(grep -c ^ /var/log/pihole/FTL.log)" + + # Extract relevant log lines + run bash -c "sed -n \"${before},${after}p\" /var/log/pihole/FTL.log" + + # Check for expected log lines + printf "%s\n" "${lines[@]}" + [[ "${lines[@]}" == *"new TCP IPv4 non-query[type=0] \"opcode\" from lo/127.0.0.1"* ]] + [[ "${lines[@]}" == *"**** got cache reply: opcode is (null) "* ]] +} + +@test "Mixed-case DNS queries are returned in the same case" { + run bash -c "dig AAAA AaaA.fTL @127.0.0.1" + printf "%s\n" "${lines[@]}" + [[ "${lines[@]}" == *"AaaA.fTL."*"IN"*"AAAA"*"fe80::1c01"* ]] +} + @test "Custom DNS records: International domains are converted to IDN form" { # äste.com ---> xn--ste-pla.com run bash -c "dig A xn--ste-pla.com +short @127.0.0.1" diff --git a/test/zone_update.py b/test/zone_update.py new file mode 100644 index 000000000..dad9d90ff --- /dev/null +++ b/test/zone_update.py @@ -0,0 +1,32 @@ +#!/bin/python3 +# Pi-hole: A black hole for Internet advertisements +# (c) 2023 Pi-hole, LLC (https://pi-hole.net) +# Network-wide ad blocking via your own hardware. +# +# FTL Engine - auxiliary files +# Send a dynamic update to the DNS server to update the a zone +# +# This file is copyright under the latest version of the EUPL. +# Please see LICENSE file for your rights under this license. + +import sys +import dns.query +import dns.update +import dns.rcode + +# Create a new update object +update = dns.update.Update('example.com') + +# Add a new A record +update.add('www.example.com', 300, 'A', '127.0.0.1') + +# Send the update to the DNS server and print the response +if sys.argv[1] == 'udp': + response = dns.query.udp(update, '127.0.0.1') + print("UDP response: " + dns.rcode.to_text(response.rcode())) +elif sys.argv[1] == 'tcp': + response = dns.query.tcp(update, '127.0.0.1') + print("TCP response: " + dns.rcode.to_text(response.rcode())) +else: + print("Invalid argument, use 'udp' or 'tcp'") + sys.exit(1)