Last active
October 1, 2022 06:01
-
-
Save philpennock/ed8f49e9edf2e8ea5dfb042e302dd502 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Copyright © 2017 Pennock Tech, LLC. | |
| // All rights reserved, except as granted under license. | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, | |
| // this list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright | |
| // notice, this list of conditions and the following disclaimer in the | |
| // documentation and/or other materials provided with the distribution. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| // POSSIBILITY OF SUCH DAMAGE. | |
| #ifdef __gnu_linux__ | |
| # define _BSD_SOURCE | |
| #endif | |
| #include <ctype.h> | |
| #ifdef __gnu_linux__ | |
| # include <getopt.h> | |
| #endif | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sysexits.h> | |
| #include <unistd.h> | |
| #ifdef __APPLE__ | |
| # define BIND_8_COMPAT /* to get <arpa/nameser_compat.h> with T_FOO macros */ | |
| #endif | |
| #include <sys/types.h> | |
| #include <netinet/in.h> | |
| #include <arpa/nameser.h> | |
| #include <netdb.h> | |
| #include <resolv.h> | |
| // Untested by me, but _should_ work | |
| #ifdef __use_openbsd__ | |
| # define NS_HFIXEDSZ HFIXEDSZ | |
| #endif | |
| // https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 | |
| #ifndef T_A6 | |
| # define T_A6 38 | |
| // missing on macOS | |
| #endif | |
| #ifndef T_OPT | |
| # define T_OPT 41 | |
| // missing on Linux | |
| #endif | |
| #ifndef T_RRSIG | |
| # define T_RRSIG 46 | |
| #endif | |
| #ifndef T_SPF | |
| # define T_SPF 99 | |
| #endif | |
| #ifndef T_TSIG | |
| # define T_TSIG 250 | |
| // missing on macOS | |
| #endif | |
| #ifndef T_URI | |
| # define T_URI 256 | |
| #endif | |
| #ifndef T_CAA | |
| # define T_CAA 257 | |
| #endif | |
| static struct qtype_num_s { | |
| char *name; | |
| int qtype; | |
| } qtypes[] = { | |
| { "a", T_A }, | |
| { "a6", T_A6 }, | |
| { "aaaa", T_AAAA }, | |
| { "afsdb", T_AFSDB }, | |
| { "any", T_ANY }, | |
| { "atma", T_ATMA }, | |
| { "axfr", T_AXFR }, | |
| { "caa", T_CAA }, | |
| { "cname", T_CNAME }, | |
| { "eid", T_EID }, | |
| { "gpos", T_GPOS }, | |
| { "hinfo", T_HINFO }, | |
| { "isdn", T_ISDN }, | |
| { "ixfr", T_IXFR }, | |
| { "key", T_KEY }, | |
| { "loc", T_LOC }, | |
| { "maila", T_MAILA }, | |
| { "mailb", T_MAILB }, | |
| { "mb", T_MB }, | |
| { "md", T_MD }, | |
| { "mf", T_MF }, | |
| { "mg", T_MG }, | |
| { "minfo", T_MINFO }, | |
| { "mr", T_MR }, | |
| { "mx", T_MX }, | |
| { "naptr", T_NAPTR }, | |
| { "nimloc", T_NIMLOC }, | |
| { "ns", T_NS }, | |
| { "nsap", T_NSAP }, | |
| { "nsap_ptr", T_NSAP_PTR }, | |
| { "null", T_NULL }, | |
| { "nxt", T_NXT }, | |
| { "opt", T_OPT }, | |
| { "ptr", T_PTR }, | |
| { "px", T_PX }, | |
| { "rp", T_RP }, | |
| { "rt", T_RT }, | |
| { "sig", T_SIG }, | |
| { "soa", T_SOA }, | |
| { "spf", T_SPF }, | |
| { "srv", T_SRV }, | |
| { "tsig", T_TSIG }, | |
| { "txt", T_TXT }, | |
| { "uri", T_URI }, | |
| { "wks", T_WKS }, | |
| { "x25", T_X25 }, | |
| }; | |
| static int qtypes_size = sizeof(qtypes) / sizeof(qtypes[0]); | |
| static int | |
| dns_type_from_string(const char * const name) | |
| { | |
| char *lname = strdup(name); // | |
| size_t len = strlen(lname); | |
| for (size_t i = 0; lname[i] && i < len; i++) { | |
| lname[i] = tolower(lname[i]); | |
| } | |
| // Expect usually A/AAAA; it's a small list, sufficiently efficient to just walk it each time. | |
| for (size_t i = 0; i < qtypes_size; i++) { | |
| if (strcmp(lname, qtypes[i].name) == 0) { | |
| free(lname); | |
| return qtypes[i].qtype; | |
| } | |
| } | |
| free(lname); | |
| return -1; | |
| } | |
| // https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml | |
| const char * | |
| rcode_to_string(uint16_t rcode) | |
| { | |
| switch (rcode) { | |
| case 0: return "NoError: No Error"; | |
| case 1: return "FormErr: Format Error"; | |
| case 2: return "ServFail: Server Failure"; | |
| case 3: return "NXDomain: Non-Existent Domain"; | |
| case 4: return "NotImp: Not Implemented"; | |
| case 5: return "Refused: Query Refused"; | |
| case 6: return "YXDomain: Name Exists when it should not"; | |
| case 7: return "YXRRSet: RR Set Exists when it should not"; | |
| case 8: return "NXRRSet: RR Set that should exist does not"; | |
| case 9: return "NotAuth: Not Authorized / Server Not Authoritative for zone"; | |
| case 10: return "NotZone: Name not contained in zone"; | |
| case 16: return "BADSIG: TSIG Signature Failure (or BADVERS: Bad OPT Version)"; | |
| case 17: return "BADKEY: Key not recognized"; | |
| case 18: return "BADTIME: Signature out of time window"; | |
| case 19: return "BADMODE: Bad TKEY Mode"; | |
| case 20: return "BADNAME: Duplicate key name"; | |
| case 21: return "BADALG: Algorithm not supported"; | |
| case 22: return "BADTRUNC: Bad Truncation"; | |
| case 23: return "BADCOOKIE: Bad/missing Server Cookie"; | |
| default: return "<unknown>"; | |
| } | |
| } | |
| const char * | |
| edns0_optcode_to_string(uint16_t optcode) | |
| { | |
| switch (optcode) { | |
| case 1: return "LLQ"; | |
| case 2: return "UL"; | |
| case 3: return "NSID"; | |
| case 4: return "Reserved"; | |
| case 5: return "DAU"; | |
| case 6: return "DHU"; | |
| case 7: return "N3U"; | |
| case 8: return "edns-client-subnet"; | |
| case 9: return "EDNS EXPIRE"; | |
| case 10: return "COOKIE"; | |
| case 11: return "edns-tcp-keepalive"; | |
| case 12: return "Padding"; | |
| case 13: return "CHAIN"; | |
| case 26946: return "DeviceID"; | |
| default: return "<unknown>"; | |
| } | |
| } | |
| struct opts { | |
| char *progname; | |
| int argv_nonoption; | |
| int qtype; | |
| bool init; | |
| bool aaonly; | |
| bool debug; | |
| bool defnames; | |
| bool dnsrch; | |
| bool dnssec; | |
| bool edns0; | |
| bool inet6; | |
| bool noaliases; | |
| bool recurse; | |
| bool use_vc; | |
| }; | |
| static void | |
| usage(struct opts *o, int exitval) | |
| { | |
| FILE *out = exitval ? stderr : stdout; | |
| fprintf(out, "Usage: %s [-<features>] [-t QTYPE] <domain> [<domain> ...]\n", o->progname); | |
| fprintf(out, " Options lower-case to turn ON, upper-case to turn OFF.\n"); | |
| fprintf(out, " a: aaonly d: debug i: init l: noaliases n: defnames\n"); | |
| fprintf(out, " r: recurse s: dnsrc v: use-vc k: dnssec\n"); | |
| fprintf(out, " 6: INET6 on, ^: off; 0: EDNS0 on, ): off\n"); | |
| exit(exitval); | |
| } | |
| static void | |
| parse_options(struct opts *o, int argc, char *argv[]) | |
| { | |
| o->argv_nonoption = 0; | |
| o->qtype = T_A; | |
| o->debug = o->aaonly = o->use_vc = o->recurse = o->defnames = o->dnsrch = o-> noaliases = o->inet6 = o->edns0 = false; | |
| o->init = true; | |
| o->progname = strrchr(argv[0], '/'); | |
| o->progname = o->progname ? o->progname+1 : argv[0]; | |
| int ch; | |
| while ((ch = getopt(argc, argv, ":aAdDhiIkKlLnNrRsSt:vV6^0)")) != -1) { | |
| switch (tolower(ch)) { | |
| case 'h': usage(o, 0); break; | |
| case '?': fprintf(stderr, "%s: unknown option -%c; try -h\n", o->progname, ch); exit(EX_USAGE); break; | |
| case ':': fprintf(stderr, "%s: missing value for option -%c\n", o->progname, ch); exit(EX_USAGE); break; | |
| #define Handle(Letter, Name) case Letter: o->Name = islower(ch) ? true : false; break | |
| Handle('a', aaonly); | |
| Handle('d', debug); | |
| Handle('i', init); | |
| Handle('k', dnssec); | |
| Handle('l', noaliases); | |
| Handle('n', defnames); | |
| Handle('r', recurse); | |
| Handle('s', dnsrch); | |
| Handle('v', use_vc); | |
| #undef Handle | |
| case 't': | |
| o->qtype = dns_type_from_string(optarg); break; | |
| if (o->qtype < 0) { | |
| fprintf(stderr, "bad DNS query type '%s'\n", optarg); | |
| exit(1); | |
| } | |
| case '6': | |
| case '^': | |
| o->inet6 = ch == '6' ? true : false; break; | |
| case '0': | |
| case ')': | |
| o->edns0 = ch == '0' ? true : false; break; | |
| } | |
| } | |
| o->argv_nonoption = optind; | |
| } | |
| static void | |
| initialize_dns(struct opts *o) | |
| { | |
| if (o->init) res_init(); | |
| #define SetOnOff(Name, Macro) do { if (o->Name) { _res.options |= (Macro); } else { _res.options &= ~(Macro); } } while (0) | |
| SetOnOff(aaonly, RES_AAONLY); | |
| SetOnOff(debug, RES_DEBUG); | |
| SetOnOff(defnames, RES_DEFNAMES); | |
| SetOnOff(dnsrch, RES_DNSRCH); | |
| SetOnOff(dnssec, RES_USE_DNSSEC); | |
| SetOnOff(edns0, RES_USE_EDNS0); | |
| SetOnOff(inet6, RES_USE_INET6); | |
| SetOnOff(noaliases, RES_NOALIASES); | |
| SetOnOff(recurse, RES_RECURSE); | |
| SetOnOff(use_vc, RES_USEVC); | |
| #undef SetOnOff | |
| } | |
| struct accumulated_flags { | |
| uint16_t id; // host order | |
| uint8_t opcode_4 :4; | |
| uint8_t rcode_4 :4; | |
| bool aa, tc, rd, ra, ad, cd; | |
| bool do_; | |
| uint16_t rcode; | |
| uint8_t edns0_version; | |
| uint16_t edns0_flags; | |
| }; | |
| static void | |
| render_accumulated_flags(struct accumulated_flags *f) | |
| { | |
| printf("\n; id=%hu edns_version=%hhu opcode=%hhu\n", f->id, f->edns0_version, f->opcode_4); | |
| printf("; RCODE: %hu\t%s\n", f->rcode, rcode_to_string(f->rcode)); | |
| #define B(Flag) (f->Flag?'Y':'n') | |
| printf("; aa=%c tc=%c rd=%c ra=%c ad=%c cd=%c do=%c\n", B(aa), B(tc), B(rd), B(ra), B(ad), B(cd), B(do_)); | |
| #undef B | |
| } | |
| // The caller should have ensured that there is a full avlen of data available at avdata onwards. | |
| static bool | |
| emit_edns0(struct accumulated_flags *aflags, | |
| uint16_t udp_payload_sz, uint32_t ext_rcode_flags, | |
| uint16_t avlen, const u_char *avdata) | |
| { | |
| printf("\n; EDNS0 UDP payload size = %hu extended rcode/flags = %u\n", udp_payload_sz, ext_rcode_flags); | |
| aflags->rcode |= (ext_rcode_flags & 0xFF000000) >> 20; // upper 8 bits of 12-bit value, from top 8 of 32 | |
| aflags->edns0_version = (ext_rcode_flags & 0x00FF0000) >> 16; | |
| aflags->edns0_flags = ext_rcode_flags & 0xFFFF; | |
| aflags->do_ = ext_rcode_flags & 0x8000 ? true : false; | |
| printf("; EDNS0 attribute/value octets: %hu\n", avlen); | |
| while (avlen > 0) { | |
| if (avlen < 4) { | |
| fprintf(stderr, "; ERROR: not enough space for A/V header %hu octets left\n", avlen); | |
| return false; | |
| } | |
| uint16_t opt_code = avdata[0] * 256 + avdata[1]; | |
| uint16_t opt_len = avdata[2] * 256 + avdata[3]; | |
| avlen -= 4; | |
| if (opt_len > avlen) { | |
| fprintf(stderr, "; ERROR; EDNS A/V option %hu claims %hu octets payload, only %hu left\n", | |
| opt_code, opt_len, avlen); | |
| return false; | |
| } | |
| printf("; A/V Option %hu (%s) Payload length %hu\n", opt_code, edns0_optcode_to_string(opt_code), opt_len); | |
| avlen -= opt_len; | |
| avdata += 4 + opt_len; | |
| } | |
| return true; | |
| } | |
| static bool | |
| maybe_emit_rrdata(struct accumulated_flags *aflags, | |
| uint16_t qtype, | |
| uint16_t rdlen, | |
| const u_char *rddata, | |
| const u_char *packet_start, | |
| const u_char *packet_past_end | |
| ) | |
| { | |
| char expanded_name[256]; | |
| int consumed; | |
| uint16_t u16_1; | |
| #define ExpandCompressed(Ptr) do { \ | |
| consumed = dn_expand(packet_start, packet_past_end, (Ptr), expanded_name, sizeof(expanded_name)); if (consumed < 0) { \ | |
| fprintf(stderr, "; failed to expand name\n"); return false; } } while (0) | |
| #define FailLength(Msg, Got) do { fprintf(stderr, "; %s, saw len %hu\n", (Msg), (Got)); return false; } while (0) | |
| #define ExtractU16(Var, Ptr) do { Var = Ptr[0] * 256 + Ptr[1]; } while (0) | |
| switch (qtype) { | |
| case T_A: | |
| if (rdlen != 4) FailLength("\"A\" record must have length 4", rdlen); | |
| printf("\t\tA\t%hhu.%hhu.%hhu.%hhu\n", rddata[0], rddata[1], rddata[2], rddata[3]); | |
| break; | |
| case T_AAAA: | |
| break; | |
| case T_NS: | |
| case T_CNAME: | |
| ExpandCompressed(rddata); | |
| printf("\t\t%s\t%s\n", (qtype == T_NS ? "NS" : "CNAME"), expanded_name); | |
| break; | |
| case T_TXT: | |
| case T_SPF: | |
| break; | |
| case T_MX: | |
| ExtractU16(u16_1, rddata); | |
| ExpandCompressed(rddata+2); | |
| printf("\t\t%hu\t%s\n", u16_1, expanded_name); | |
| break; | |
| case T_PTR: | |
| break; | |
| case T_CAA: | |
| break; | |
| default: | |
| // nothing, we are happy to just skip the full dump | |
| break; | |
| } | |
| return true; | |
| #undef ExpandCompressed | |
| #undef FailLength | |
| #undef ExtractU16 | |
| } | |
| static bool | |
| perform_resolution(struct opts *o, const char *name) | |
| { | |
| int res_len; | |
| u_char buf[65536]; | |
| char expanded_name[256]; | |
| int buflen_i = (int) sizeof(buf); | |
| res_len = res_search(name, C_IN, o->qtype, buf, buflen_i); | |
| if (res_len < 0) { | |
| fprintf(stderr, "%s: resolving for '%s' failed: [%d] %s\n", o->progname, name, h_errno, hstrerror(h_errno)); | |
| return false; | |
| } | |
| if (res_len < NS_HFIXEDSZ) { | |
| fprintf(stderr, "%s: resolving for '%s' response len (%d) too short\n", o->progname, name, res_len); | |
| return false; | |
| } | |
| HEADER* hdr = (HEADER*) buf; | |
| printf(";;; id=%hu %s\taa=%d tc=%d rd=%d ra=%d ad=%d cd=%d\n", | |
| ntohs(hdr->id), hdr->qr? "response" : "query", hdr->aa, hdr->tc, hdr->rd, hdr->ra, hdr->ad, hdr->cd); | |
| struct accumulated_flags accum = { | |
| .id = ntohs(hdr->id), | |
| .aa = hdr->aa, | |
| .tc = hdr->tc, | |
| .rd = hdr->rd, | |
| .ra = hdr->ra, | |
| .ad = hdr->ad, | |
| .cd = hdr->cd, | |
| .opcode_4 = hdr->opcode, | |
| .rcode_4 = hdr->rcode, | |
| .rcode = hdr->rcode, | |
| }; | |
| uint16_t count_questions = ntohs(hdr->qdcount); | |
| uint16_t count_answers = ntohs(hdr->ancount); | |
| uint16_t count_authority = ntohs(hdr->nscount); | |
| uint16_t count_additional = ntohs(hdr->arcount); | |
| struct { | |
| const int next; | |
| const uint16_t *count; | |
| const bool full_rr; | |
| const char *label; | |
| } sections[5] = { | |
| { 1, &count_questions, false, "QUESTION" }, | |
| { 2, &count_answers, true, "ANSWERS" }, | |
| { 3, &count_authority, true, "AUTHORITY" }, | |
| { -1, &count_additional, true, "ADDITIONAL" }, | |
| }; | |
| bool okay = true; | |
| u_char *extract_from = buf + NS_HFIXEDSZ; | |
| for (int section = 0; section >= 0; section = sections[section].next) { | |
| int rrexpect = *sections[section].count; | |
| printf("\n;;; %s : %d entries\n", sections[section].label, rrexpect); | |
| while (rrexpect-- > 0) { | |
| int consumed = | |
| dn_expand(buf, buf + res_len, extract_from, expanded_name, sizeof(expanded_name)); | |
| #define parse_error(Label) do { okay = false; \ | |
| fprintf(stderr, ";;; %s item %d\n", (Label), *sections[section].count - rrexpect); \ | |
| goto PARSE_ABORT; } while(0) | |
| if (consumed < 0) parse_error("error expanding"); | |
| extract_from += consumed; | |
| if ((extract_from + 4) > buf + res_len) parse_error("no room for type/class in"); | |
| uint16_t qt = extract_from[0] * 256 + extract_from[1]; | |
| uint16_t qc = extract_from[2] * 256 + extract_from[3]; | |
| extract_from += 4; | |
| if (sections[section].full_rr) { | |
| if (extract_from + 6 > buf + res_len) parse_error("no room for rest of RR"); | |
| uint32_t attl = extract_from[0] * 256 * 256 * 256 + | |
| extract_from[1] * 256 * 256 + | |
| extract_from[2] * 256 + | |
| extract_from[3]; | |
| uint16_t rdlen = extract_from[4] * 256 + extract_from[5]; | |
| extract_from += 6 + rdlen; | |
| if (extract_from > buf + res_len) parse_error("no room for rrdata"); | |
| // EDNS0 is Option 41 and not in headers as a const (non-portably? as enum) | |
| if (qt == 41) { | |
| // expanded_name should be empty, type is 41, the rest is passed along | |
| if (!emit_edns0(&accum, qc, attl, rdlen, extract_from-rdlen)) { | |
| okay = false; | |
| goto PARSE_ABORT; | |
| } | |
| } else { // regular | |
| printf("%s\ttype=%hu class=%hu ttl=%u rrlen=%hu%s\n", | |
| expanded_name, qt, qc, attl, rdlen, | |
| // Only stuff we flag without decoding here; decoding into maybe_emit_rrdata | |
| (qt == T_RRSIG ? " [RRSIG]" : "") | |
| ); | |
| if (!maybe_emit_rrdata(&accum, qt, rdlen, extract_from-rdlen, buf, buf+res_len)) { | |
| okay = false; | |
| goto PARSE_ABORT; | |
| } | |
| } | |
| } else { | |
| printf("%s\ttype=%hu class=%hu\n", expanded_name, qt, qc); | |
| } | |
| if (extract_from > buf + res_len) parse_error("gone past end of packet in"); | |
| } | |
| } | |
| render_accumulated_flags(&accum); | |
| #undef parse_error | |
| PARSE_ABORT: | |
| return okay; | |
| } | |
| int | |
| main(int argc, char *argv[]) | |
| { | |
| struct opts p_o; | |
| parse_options(&p_o, argc, argv); | |
| if (p_o.argv_nonoption < 1 || p_o.argv_nonoption == argc) { | |
| usage(&p_o, EX_USAGE); | |
| } | |
| initialize_dns(&p_o); | |
| int exval = 0; | |
| for (int i = p_o.argv_nonoption; i < argc; i++) { | |
| if (!perform_resolution(&p_o, argv[i])) { | |
| exval = 1; | |
| } | |
| } | |
| return exval; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment