From 6b17335209639a56f214d011eaed4ebcde8dd276 Mon Sep 17 00:00:00 2001
From: Simon Kelley <simon@thekelleys.org.uk>
Date: Tue, 8 May 2018 18:32:14 +0100
Subject: [PATCH 04/10] Add packet-dump debugging facility.

Signed-off-by: Kevin Darbyshire-Bryant <ldir@darbyshire-bryant.me.uk>
---
 CHANGELOG      |   6 ++
 Makefile       |   2 +-
 bld/Android.mk |   3 +-
 man/dnsmasq.8  |   7 ++
 src/config.h   |  16 ++++-
 src/dnsmasq.c  |  16 ++++-
 src/dnsmasq.h  |  29 +++++++-
 src/dump.c     | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/forward.c  |  37 ++++++++--
 src/option.c   |  14 ++++
 10 files changed, 329 insertions(+), 11 deletions(-)
 create mode 100644 src/dump.c

--- a/CHANGELOG
+++ b/CHANGELOG
@@ -17,6 +17,12 @@ version 2.80
         Fix DHCP broken-ness when --no-ping AND --dhcp-sequential-ip
 	are set. Thanks to Daniel Miess for help with this.
 
+	Add a facilty to store DNS packets sent/recieved in a
+	pcap-format file for later debugging. The file location
+	is given by the --dumpfile option, and a bitmap controlling
+	which packets should be dumped is given by the --dumpmask
+	option.
+
 
 version 2.79
 	Fix parsing of CNAME arguments, which are confused by extra spaces.
--- a/Makefile
+++ b/Makefile
@@ -76,7 +76,7 @@ objs = cache.o rfc1035.o util.o option.o
        helper.o tftp.o log.o conntrack.o dhcp6.o rfc3315.o \
        dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o \
        domain.o dnssec.o blockdata.o tables.o loop.o inotify.o \
-       poll.o rrfilter.o edns0.o arp.o crypto.o
+       poll.o rrfilter.o edns0.o arp.o crypto.o dump.o
 
 hdrs = dnsmasq.h config.h dhcp-protocol.h dhcp6-protocol.h \
        dns-protocol.h radv-protocol.h ip6addr.h
--- a/bld/Android.mk
+++ b/bld/Android.mk
@@ -10,7 +10,8 @@ LOCAL_SRC_FILES :=  bpf.c cache.c dbus.c
 		    dhcp6.c rfc3315.c dhcp-common.c outpacket.c \
 		    radv.c slaac.c auth.c ipset.c domain.c \
 	            dnssec.c dnssec-openssl.c blockdata.c tables.c \
-		    loop.c inotify.c poll.c rrfilter.c edns0.c arp.c crypto.c
+		    loop.c inotify.c poll.c rrfilter.c edns0.c arp.c \
+		    crypto.c dump.c
 
 LOCAL_MODULE := dnsmasq
 
--- a/man/dnsmasq.8
+++ b/man/dnsmasq.8
@@ -647,6 +647,13 @@ V4 mapped IPv6 addresses, which have a r
 The address range can be of the form
 <ip address>,<ip address> or <ip address>/<netmask> in both forms of the option.
 .TP
+.B --dumpfile=<path/to/file>
+Specify the location of a pcap-format file which dnsmasq uses to dump copies of network packets for debugging purposes. If the file exists when dnsmasq starts, it is not deleted; new packets are added to the end.
+.TP
+.B --dumpmask=<mask>
+Specify which types of packets should be added to the dumpfile. The argument should be the OR of the bitmasks for each type of packet to be dumped: it can be specified in hex by preceding the number with 0x in  the normal way. Each time a packet is written to the dumpfile, dnsmasq logs the packet sequence and the mask
+representing its type. The current types are: 0x0001 - DNS queries from clients 0x0002 DNS replies to clients 0x0004 - DNS queries to upstream 0x0008 - DNS replies from upstream 0x0010 - queries send upstream for DNSSEC validation 0x0020 - replies to queries for DNSSEC validation 0x0040 - replies to client queries which fail DNSSEC validation 0x0080 replies to queries for DNSSEC validation which fail validation.
+.TP
 .B --add-mac[=base64|text]
 Add the MAC address of the requestor to DNS queries which are
 forwarded upstream. This may be used to DNS filtering by the upstream
--- a/src/config.h
+++ b/src/config.h
@@ -117,6 +117,9 @@ HAVE_AUTH
 HAVE_DNSSEC
    include DNSSEC validator.
 
+HAVE_DUMPFILE
+   include code to dump packets to a libpcap-format file for debugging.
+
 HAVE_LOOP
    include functionality to probe for and remove DNS forwarding loops.
 
@@ -132,6 +135,7 @@ NO_DHCP6
 NO_SCRIPT
 NO_LARGEFILE
 NO_AUTH
+NO_DUMPFILE
 NO_INOTIFY
    these are available to explicitly disable compile time options which would 
    otherwise be enabled automatically (HAVE_IPV6, >2Gb file sizes) or 
@@ -164,6 +168,7 @@ RESOLVFILE
 #define HAVE_AUTH
 #define HAVE_IPSET 
 #define HAVE_LOOP
+#define HAVE_DUMPFILE
 
 /* Build options which require external libraries.
    
@@ -363,6 +368,10 @@ HAVE_SOCKADDR_SA_LEN
 #undef HAVE_LOOP
 #endif
 
+#ifdef NO_DUMPFILE
+#undef HAVE_DUMPFILE
+#endif
+
 #if defined (HAVE_LINUX_NETWORK) && !defined(NO_INOTIFY)
 #define HAVE_INOTIFY
 #endif
@@ -451,8 +460,11 @@ static char *compile_opts =
 #ifndef HAVE_INOTIFY
 "no-"
 #endif
-"inotify";
-
+"inotify "
+#ifndef HAVE_DUMPFILE
+"no-"
+#endif
+"dumpfile";
 
 #endif
 
--- a/src/dnsmasq.c
+++ b/src/dnsmasq.c
@@ -366,7 +366,16 @@ int main (int argc, char **argv)
   else
     daemon->inotifyfd = -1;
 #endif
-       
+
+  if (daemon->dump_file)
+#ifdef HAVE_DUMPFILE
+    dump_init();
+  else 
+    daemon->dumpfd = -1;
+#else
+  die(_("Packet dumps not available: set HAVE_DUMP in src/config.h"), NULL, EC_BADCONF);
+#endif
+  
   if (option_bool(OPT_DBUS))
 #ifdef HAVE_DBUS
     {
@@ -1424,6 +1433,11 @@ static void async_event(int pipe, time_t
 
 	if (daemon->runfile)
 	  unlink(daemon->runfile);
+
+#ifdef HAVE_DUMPFILE
+	if (daemon->dumpfd != -1)
+	  close(daemon->dumpfd);
+#endif
 	
 	my_syslog(LOG_INFO, _("exiting on receipt of SIGTERM"));
 	flush_log();
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -119,6 +119,9 @@ typedef unsigned long long u64;
 #include <net/if_arp.h>
 #include <netinet/in_systm.h>
 #include <netinet/ip.h>
+#ifdef HAVE_IPV6
+#include <netinet/ip6.h>
+#endif
 #include <netinet/ip_icmp.h>
 #include <sys/uio.h>
 #include <syslog.h>
@@ -598,6 +601,16 @@ struct hostsfile {
   unsigned int index; /* matches to cache entries for logging */
 };
 
+/* packet-dump flags */
+#define DUMP_QUERY     0x0001
+#define DUMP_REPLY     0x0002
+#define DUMP_UP_QUERY  0x0004
+#define DUMP_UP_REPLY  0x0008
+#define DUMP_SEC_QUERY 0x0010
+#define DUMP_SEC_REPLY 0x0020
+#define DUMP_BOGUS     0x0040
+#define DUMP_SEC_BOGUS 0x0080
+
 
 /* DNSSEC status values. */
 #define STAT_SECURE             1
@@ -1020,14 +1033,14 @@ extern struct daemon {
   unsigned int duid_enterprise, duid_config_len;
   unsigned char *duid_config;
   char *dbus_name;
+  char *dump_file;
+  int dump_mask;
   unsigned long soa_sn, soa_refresh, soa_retry, soa_expiry;
 #ifdef OPTION6_PREFIX_CLASS 
   struct prefix_class *prefix_classes;
 #endif
 #ifdef HAVE_DNSSEC
   struct ds_config *ds;
-  int dnssec_no_time_check;
-  int back_to_the_future;
   char *timestamp_file;
 #endif
 
@@ -1040,6 +1053,8 @@ extern struct daemon {
   char *workspacename; /* ditto */
   char *rr_status; /* flags for individual RRs */
   int rr_status_sz;
+  int dnssec_no_time_check;
+  int back_to_the_future;
 #endif
   unsigned int local_answer, queries_forwarded, auth_answer;
   struct frec *frec_list;
@@ -1094,6 +1109,10 @@ extern struct daemon {
   char *addrbuff;
   char *addrbuff2; /* only allocated when OPT_EXTRALOG */
 
+#ifdef HAVE_DUMPFILE
+  /* file for packet dumps. */
+  int dumpfd;
+#endif
 } *daemon;
 
 /* cache.c */
@@ -1588,3 +1607,9 @@ int check_source(struct dns_header *head
 /* arp.c */
 int find_mac(union mysockaddr *addr, unsigned char *mac, int lazy, time_t now);
 int do_arp_script_run(void);
+
+/* dump.c */
+#ifdef HAVE_DUMPFILE
+void dump_init(void);
+void dump_packet(int mask, void *packet, size_t len, union mysockaddr *src, union mysockaddr *dst);
+#endif
--- /dev/null
+++ b/src/dump.c
@@ -0,0 +1,210 @@
+/* dnsmasq is Copyright (c) 2000-2018 Simon Kelley
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; version 2 dated June, 1991, or
+   (at your option) version 3 dated 29 June, 2007.
+ 
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+     
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "dnsmasq.h"
+
+#ifdef HAVE_DUMPFILE
+
+static u32 packet_count;
+
+/* https://wiki.wireshark.org/Development/LibpcapFileFormat */
+struct pcap_hdr_s {
+        u32 magic_number;   /* magic number */
+        u16 version_major;  /* major version number */
+        u16 version_minor;  /* minor version number */
+        u32 thiszone;       /* GMT to local correction */
+        u32 sigfigs;        /* accuracy of timestamps */
+        u32 snaplen;        /* max length of captured packets, in octets */
+        u32 network;        /* data link type */
+};
+
+struct pcaprec_hdr_s {
+        u32 ts_sec;         /* timestamp seconds */
+        u32 ts_usec;        /* timestamp microseconds */
+        u32 incl_len;       /* number of octets of packet saved in file */
+        u32 orig_len;       /* actual length of packet */
+};
+
+
+void dump_init(void)
+{
+  struct stat buf;
+  struct pcap_hdr_s header;
+  struct pcaprec_hdr_s pcap_header;
+
+  packet_count = 0;
+  
+  if (stat(daemon->dump_file, &buf) == -1)
+    {
+      /* doesn't exist, create and add header */
+      header.magic_number = 0xa1b2c3d4;
+      header.version_major = 2;
+      header.version_minor = 4;
+      header.thiszone = 0;
+      header.sigfigs = 0;
+      header.snaplen = daemon->edns_pktsz + 200; /* slop for IP/UDP headers */
+      header.network = 101; /* DLT_RAW http://www.tcpdump.org/linktypes.html */
+
+      if (errno != ENOENT ||
+	  (daemon->dumpfd = creat(daemon->dump_file, S_IRUSR | S_IWUSR)) == -1 ||
+	  !read_write(daemon->dumpfd, (void *)&header, sizeof(header), 0))
+	die(_("cannot create %s: %s"), daemon->dump_file, EC_FILE);
+    }
+  else if ((daemon->dumpfd = open(daemon->dump_file, O_APPEND | O_RDWR)) == -1 ||
+	   !read_write(daemon->dumpfd, (void *)&header, sizeof(header), 1) ||
+	   header.magic_number != 0xa1b2c3d4)
+    die(_("cannot access %s: %s"), daemon->dump_file, EC_FILE);
+  else
+    {
+      /* count existing records */
+      while (read_write(daemon->dumpfd, (void *)&pcap_header, sizeof(pcap_header), 1))
+	{
+	  lseek(daemon->dumpfd, pcap_header.incl_len, SEEK_CUR);
+	  packet_count++;
+	}
+    }
+}
+
+void dump_packet(int mask, void *packet, size_t len, union mysockaddr *src, union mysockaddr *dst)
+{
+  struct ip ip;
+#ifdef HAVE_IPV6
+  struct ip6_hdr ip6;
+  int family;
+#endif
+  struct udphdr {
+    u16 uh_sport;               /* source port */
+    u16 uh_dport;               /* destination port */
+    u16 uh_ulen;                /* udp length */
+    u16 uh_sum;                 /* udp checksum */
+  } udp;
+  struct pcaprec_hdr_s pcap_header;
+  struct timeval time;
+  u32 i, sum;
+  void *iphdr;
+  size_t ipsz;
+  int rc;
+  
+  if (daemon->dumpfd == -1 || !(mask & daemon->dump_mask))
+    return;
+  
+  /* So wireshark can Id the packet. */
+  udp.uh_sport = udp.uh_dport = htons(NAMESERVER_PORT);
+
+#ifdef HAVE_IPV6
+  if (src)
+    family = src->sa.sa_family;
+  else
+    family = dst->sa.sa_family;
+
+  if (family == AF_INET6)
+    {
+      iphdr = &ip6;
+      ipsz = sizeof(ip6);
+      memset(&ip6, 0, sizeof(ip6));
+      
+      ip6.ip6_vfc = 6 << 4;
+      ip6.ip6_plen = htons(sizeof(struct udphdr) + len);
+      ip6.ip6_nxt = IPPROTO_UDP;
+      ip6.ip6_hops = 64;
+
+      if (src)
+	{
+	  memcpy(&ip6.ip6_src, &src->in6.sin6_addr, IN6ADDRSZ);
+	  udp.uh_sport = src->in6.sin6_port;
+	}
+      
+      if (dst)
+	{
+	  memcpy(&ip6.ip6_dst, &dst->in6.sin6_addr, IN6ADDRSZ);
+	  udp.uh_dport = dst->in6.sin6_port;
+	}
+            
+      /* start UDP checksum */
+      for (sum = 0, i = 0; i < IN6ADDRSZ; i++)
+	sum += ((u16 *)&ip6.ip6_src)[i];
+    }
+  else
+#endif
+    {
+      iphdr = &ip;
+      ipsz = sizeof(ip);
+      memset(&ip, 0, sizeof(ip));
+      
+      ip.ip_v = IPVERSION;
+      ip.ip_hl = sizeof(struct ip) / 4;
+      ip.ip_len = htons(sizeof(struct ip) + sizeof(struct udphdr) + len); 
+      ip.ip_ttl = IPDEFTTL;
+      ip.ip_p = IPPROTO_UDP;
+      
+      if (src)
+	{
+	  ip.ip_src = src->in.sin_addr;
+	  udp.uh_sport = src->in.sin_port;
+	}
+
+      if (dst)
+	{
+	  ip.ip_dst = dst->in.sin_addr;
+	  udp.uh_dport = dst->in.sin_port;
+	}
+      
+      ip.ip_sum = 0;
+      for (sum = 0, i = 0; i < sizeof(struct ip) / 2; i++)
+	sum += ((u16 *)&ip)[i];
+      while (sum >> 16)
+	sum = (sum & 0xffff) + (sum >> 16);  
+      ip.ip_sum = (sum == 0xffff) ? sum : ~sum;
+      
+      /* start UDP checksum */
+      sum = ip.ip_src.s_addr & 0xffff;
+      sum += (ip.ip_src.s_addr >> 16) & 0xffff;
+      sum += ip.ip_dst.s_addr & 0xffff;
+      sum += (ip.ip_dst.s_addr >> 16) & 0xffff;
+    }
+  
+  if (len & 1)
+    ((unsigned char *)packet)[len] = 0; /* for checksum, in case length is odd. */
+
+  udp.uh_sum = 0;
+  udp.uh_ulen = htons(sizeof(struct udphdr) + len);
+  sum += htons(IPPROTO_UDP);
+  sum += htons(sizeof(struct udphdr) + len);
+  for (i = 0; i < sizeof(struct udphdr)/2; i++)
+    sum += ((u16 *)&udp)[i];
+  for (i = 0; i < (len + 1) / 2; i++)
+    sum += ((u16 *)packet)[i];
+  while (sum >> 16)
+    sum = (sum & 0xffff) + (sum >> 16);
+  udp.uh_sum = (sum == 0xffff) ? sum : ~sum;
+
+  rc = gettimeofday(&time, NULL);
+  pcap_header.ts_sec = time.tv_sec;
+  pcap_header.ts_usec = time.tv_usec;
+  pcap_header.incl_len = pcap_header.orig_len = ipsz + sizeof(udp) + len;
+  
+  if (rc == -1 ||
+      !read_write(daemon->dumpfd, (void *)&pcap_header, sizeof(pcap_header), 0) ||
+      !read_write(daemon->dumpfd, iphdr, ipsz, 0) ||
+      !read_write(daemon->dumpfd, (void *)&udp, sizeof(udp), 0) ||
+      !read_write(daemon->dumpfd, (void *)packet, len, 0))
+    my_syslog(LOG_ERR, _("failed to write packet dump"));
+  else
+    my_syslog(LOG_INFO, _("dumping UDP packet %u mask 0x%04x"), ++packet_count, mask);
+
+}
+
+#endif
--- a/src/forward.c
+++ b/src/forward.c
@@ -508,6 +508,10 @@ static int forward_query(int udpfd, unio
 	    
 	      if (errno == 0)
 		{
+#ifdef HAVE_DUMPFILE
+		  dump_packet(DUMP_UP_QUERY, (void *)header, plen, NULL, &start->addr);
+#endif
+		  
 		  /* Keep info in case we want to re-send this packet */
 		  daemon->srv_save = start;
 		  daemon->packet_len = plen;
@@ -769,7 +773,7 @@ void reply_query(int fd, int family, tim
 #endif
   
   header = (struct dns_header *)daemon->packet;
-  
+
   if (n < (int)sizeof(struct dns_header) || !(header->hb3 & HB3_QR))
     return;
   
@@ -796,6 +800,12 @@ void reply_query(int fd, int family, tim
   if (!(forward = lookup_frec(ntohs(header->id), hash)))
     return;
   
+#ifdef HAVE_DUMPFILE
+  dump_packet((forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) ? DUMP_SEC_REPLY : DUMP_UP_REPLY,
+	      (void *)header, n, &serveraddr, NULL);
+#endif
+  
+  
   /* log_query gets called indirectly all over the place, so 
      pass these in global variables - sorry. */
   daemon->log_display_id = forward->log_id;
@@ -934,6 +944,11 @@ void reply_query(int fd, int family, tim
 		    status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class, 
 						   !option_bool(OPT_DNSSEC_IGN_NS) && (server->flags & SERV_DO_DNSSEC),
 						   NULL, NULL);
+#ifdef HAVE_DUMPFILE
+		  if (status == STAT_BOGUS)
+		    dump_packet((forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) ? DUMP_SEC_BOGUS : DUMP_BOGUS,
+				header, (size_t)n, &serveraddr, NULL);
+#endif
 		}
 	      
 	      /* Can't validate, as we're missing key data. Put this
@@ -1060,6 +1075,11 @@ void reply_query(int fd, int family, tim
 				setsockopt(fd, SOL_SOCKET, SO_MARK, &mark, sizeof(unsigned int));
 			    }
 #endif
+			  
+#ifdef HAVE_DUMPFILE
+			  dump_packet(DUMP_SEC_QUERY, (void *)header, (size_t)nn, NULL, &server->addr);
+#endif
+			  
 			  while (retry_send(sendto(fd, (char *)header, nn, 0, 
 						   &server->addr.sa, 
 						   sa_len(&server->addr)))); 
@@ -1114,8 +1134,8 @@ void reply_query(int fd, int family, tim
 	      bogusanswer = 1;
 	    }
 	}
-#endif     
-      
+#endif
+
       /* restore CD bit to the value in the query */
       if (forward->flags & FREC_CHECKING_DISABLED)
 	header->hb4 |= HB4_CD;
@@ -1141,6 +1161,11 @@ void reply_query(int fd, int family, tim
 	      nn = resize_packet(header, nn, NULL, 0);
 	    }
 #endif
+
+#ifdef HAVE_DUMPFILE
+	  dump_packet(DUMP_REPLY, daemon->packet, (size_t)nn, NULL, &forward->source);
+#endif
+	  
 	  send_from(forward->fd, option_bool(OPT_NOWILD) || option_bool (OPT_CLEVERBIND), daemon->packet, nn, 
 		    &forward->source, &forward->dest, forward->iface);
 	}
@@ -1394,7 +1419,11 @@ void receive_query(struct listener *list
      pass these in global variables - sorry. */
   daemon->log_display_id = ++daemon->log_id;
   daemon->log_source_addr = &source_addr;
-  
+
+#ifdef HAVE_DUMPFILE
+  dump_packet(DUMP_QUERY, daemon->packet, (size_t)n, &source_addr, NULL);
+#endif
+	  
   if (extract_request(header, (size_t)n, daemon->namebuff, &type))
     {
 #ifdef HAVE_AUTH
--- a/src/option.c
+++ b/src/option.c
@@ -161,6 +161,8 @@ struct myoption {
 #define LOPT_TFTP_MTU      349
 #define LOPT_REPLY_DELAY   350
 #define LOPT_RAPID_COMMIT  351
+#define LOPT_DUMPFILE      352
+#define LOPT_DUMPMASK      353
  
 #ifdef HAVE_GETOPT_LONG
 static const struct option opts[] =  
@@ -327,6 +329,8 @@ static const struct myoption opts[] =
     { "dhcp-ttl", 1, 0 , LOPT_DHCPTTL },
     { "dhcp-reply-delay", 1, 0, LOPT_REPLY_DELAY },
     { "dhcp-rapid-commit", 0, 0, LOPT_RAPID_COMMIT },
+    { "dumpfile", 1, 0, LOPT_DUMPFILE },
+    { "dumpmask", 1, 0, LOPT_DUMPMASK },
     { NULL, 0, 0, 0 }
   };
 
@@ -500,6 +504,8 @@ static struct {
   { LOPT_DHCPTTL, ARG_ONE, "<ttl>", gettext_noop("Set TTL in DNS responses with DHCP-derived addresses."), NULL }, 
   { LOPT_REPLY_DELAY, ARG_ONE, "<integer>", gettext_noop("Delay DHCP replies for at least number of seconds."), NULL },
   { LOPT_RAPID_COMMIT, OPT_RAPID_COMMIT, NULL, gettext_noop("Enables DHCPv4 Rapid Commit option."), NULL },
+  { LOPT_DUMPFILE, ARG_ONE, "<path>", gettext_noop("Path to debug packet dump file"), NULL },
+  { LOPT_DUMPMASK, ARG_ONE, "<hex>", gettext_noop("Mask which packets to dump"), NULL },
   { 0, 0, NULL, NULL, NULL }
 }; 
 
@@ -1811,6 +1817,14 @@ static int one_opt(int option, char *arg
 	ret_err(_("bad MX target"));
       break;
 
+    case LOPT_DUMPFILE:  /* --dumpfile */
+      daemon->dump_file = opt_string_alloc(arg);
+      break;
+
+    case LOPT_DUMPMASK:  /* --dumpmask */
+      daemon->dump_mask = strtol(arg, NULL, 0);
+      break;
+      
 #ifdef HAVE_DHCP      
     case 'l':  /* --dhcp-leasefile */
       daemon->lease_file = opt_string_alloc(arg);
